Based Features#
Intersection Types#
Using the &
operator or basedtyping.Intersection
you can denote intersection types:
class Growable(ABC, Generic[T]):
@abstractmethod
def add(self, item: T): ...
class Resettable(ABC):
@abstractmethod
def reset(self): ...
def f(x: Resettable & Growable[str]):
x.reset()
x.add("first")
Type Joins#
Mypy joins types to their common base type:
a: int
b: str
reveal_type(a if bool() else b) # Revealed type is "builtins.object"
Basedmypy joins types into unions instead:
a: int
b: str
reveal_type(a if bool() else b) # Revealed type is "int | str"
Bare Literals#
Literal
is so cumbersome! Just use a bare literal instead:
class Color(Enum):
RED = auto()
a: 1 | 2
b: True | Color.RED
Default Return Type#
The default return type of functions is None
instead of Any
:
(configurable with the default_return
option.)
def f(name: str):
print(f"Hello, {name}!")
reveal_type(f) # (str) -> None
Generic TypeVar
Bounds#
Basedmpy allows the bounds of TypeVar
s to be generic.
So you are able to have functions with polymorphic generic parameters:
E = TypeVar("E")
I = TypeVar("I", bound=Iterable[E])
def foo(i: I, e: E) -> I:
assert e not in i
return i
reveal_type(foo(["based"], "mypy")) # N: Revealed type is "list[str]"
reveal_type(foo({1, 2}, 3)) # N: Revealed type is "set[int]"
Reinvented type guards#
TypeGuard
acts similar to cast
, which is often sub-optimal and dangerous:
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
l1: list[object] = []
l2 = l1
if is_str_list(l1):
l2.append(100)
reveal_type(l1[0]) # Revealed type is "str", at runtime it is 100
class A: ...
class B(A): ...
def is_a(val: object) -> TypeGuard[A]: ...
b = B()
if is_a(b):
reveal_type(b) # A, not B
Basedmypy introduces a simpler and more powerful denotation for type-guards, and changes their behavior to be safer.
def is_int(value: object) -> value is int: ...
Type-guards don’t widen:
a: bool
if is_int(a):
reveal_type(a) # Revealed type is "bool"
Type-guards narrow in the negative case:
a: int | str
if is_int(a):
reveal_type(a) # Revealed type is "int"
else:
reveal_type(a) # Revealed type is "str"
Type-guards work on the implicit self
and cls
parameters:
class A:
def guard(self) -> self is B: ...
class B(A): ...
a = A()
if a.guard():
reveal_type(a) # Revealed type is "B"
Invalid type-guards show an error:
def guard(x: str) -> x is int: # error: A type-guard's type must be assignable to its parameter's type.
Type-guards that only narrow when returning true are denoted as:
def is_positive_int(x: object) -> x is int if True else False:
return isinstance(x, int) and x > 0
i: int | None
if is_positive_int(i):
reveal_type(i) # Revealed type is "int"
else:
reveal_type(i) # Revealed type is "int | None"
If you want to achieve something similar to the old TypeGuard
:
def as_str_list(val: list[object]) -> list[str] | None:
return (
cast(list[str], val)
if all(isinstance(x, str) for x in val)
else None
)
a: list[object]
if (str_a := as_str_list(a)) is not None:
...
# or
def is_str_list(val: list[object]) -> bool:
return all(isinstance(x, str) for x in val)
a: list[object]
if is_str_list(a):
str_a = cast(list[str], a)
...
Overload Implementation Inference#
The types in overload implementations (including properties) can be inferred:
@overload
def f(a: int) -> str: ...
@overload
def f(a: str) -> int: ...
def f(a):
reveal_type(a) # int | str
return None # error: expected str | int
class A:
@property
def foo(self) -> int: ...
@foo.setter
def foo(self, value): ... # no need for annotations
Infer Function Parameters#
Infer the type of a function parameter from its default value:
def f(a=1, b=True):
reveal_type((a, b)) # (int, bool)
Covariant Mapping key type#
The key type of Mapping
is fixed to be covariant:
a: Mapping[str, str]
b: Mapping[object, object] = a # no error
Tuple Literal Types#
Basedmypy allows denotation of tuple types with tuple literals:
a: (int, str) = (1, "a")
Types in Messages#
Basedmypy makes significant changes to error and info messages, consider:
T = TypeVar("T", bound=int)
def f(a: T, b: list[str | 1 | 2]) -> Never:
reveal_type((a, b))
reveal_type(f)
Mypy shows:
Revealed type is "Tuple[T`-1, Union[builtins.str, Literal[1], Literal[2]]]"
Revealed type is "def [T <: builtins.int] (a: T`-1, b: Union[builtins.str, Literal[1], Literal[2]]) -> <nothing>"
Basedmypy shows:
Revealed type is "(T@f, str | 1 | 2)"
Revealed type is "def [T: int] (a: T, b: str | 1 | 2) -> Never"
Reveal Type Narrowed#
The defined type of a variable will be shown in the message for reveal_type:
a: object
a = 1
reveal_type(a) # Revealed type is "int" (narrowed from "object")
Checked f-strings#
f"{None:0>2}" # error: The type "None" doesn't support format-specifiers
f"{date(1,1,1):%}" # error: Invalid trailing '%', escape with '%%'
f"{'s':.2f}" # error: Incompatible types in string interpolation (expression has type "str", placeholder has type "int | float | complex")
Narrow On Initial Assignment#
When a variable definition has an explicit annotation, the initialization value will be used to narrow it’s type:
a: object = 1
reveal_type(a) # Revealed type is "int"
Annotations in Functions#
Basedmypy handles type annotations in function bodies as unevaluated:
def f():
a: list[int] # no error, this annotation isn't evaluated
Checked Argument Names#
Regex Checks#
re.compile("as(df") # error: missing ), unterminated subpattern at position 0 [regex]
if m := re.search("(a)?(b)", s):
reveal_type(m.groups()) # Revealed type is "(str | None, str)"
if m := re.search("(?P<foo>a)", s):
reveal_type(m.group("foo")
reveal_type(m.group("bar") # error: no such group: 'bar' [regex]
Helpful String Check#
class A: ...
f"{A()}" # error: The type "A" doesn't define a __str__ or __format__ method [unhelpful-string]
f"{print("hi")}" # error: The string for "None" isn't helpful for a user-facing message [unhelpful-string]