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"
Based Callable¶
Basedmypy supports callable and function syntax types:
a: "(int) -> str" = lambda x: str(x)  # Callable
b: "def (int) -> str" = lambda x: str(x)  # FunctionType
In mypy, all Callables are assumed to be functions (FunctionType/builtins.function), but this is not the case for instances that have a __call__ method.
Basedmypy corrects this by separating Callable and FunctionType:
class A:
    def __call__(self, i: int) -> str: ...
a: "(int) -> str" = A()
a.__name__  # error: "() -> int" has no attribute "__name__"  [attr-defined]
b: "def (int) -> str" = lambda i: ""
b.__name__  # okay: `FunctionType` has a `__name__` attribute
Basedmypy warns against unsafe and ambiguous assignments of callables on classes:
class A:
    a: "() -> int" = lambda: 10  # error: Don't assign a "FunctionType" via the class, it will become a "MethodType"
Additionally, a Protocol _NamedCallable is introduced to represent the union of all ‘named’ callable implementations:
class A:
    def f(self): ...
reveal_type(A.f)  # "def (self: A) -> None"
reveal_type(A().f)  # "_NamedCallable & () -> None"
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 TypeVars 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]"
TypeVar usages work properly¶
mypy allows various invalid usages of TypeVar, which are corrected in basedmypy.
it’s invalid to provide variance to a constrained TypeVar because they aren’t generic, they
represent a set of choices that the TypeVar can be replaced with:
E = TypeVar("E", int, str, covariant=True)  # mypy doesn't report the error here
G = TypeVar("G", int, str)
class P(Protocol[G]):  # mypy reports an invalid error here
    def f() -> E: ...
class A[T: (object, str)]: ...
a = A[int]()  # mypy doesn't report the error here
class B[T: int]: ...
type C = B[object]  # mypy doesn't report the error here
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)
    ...
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]):
    reveal_type((a, b))
reveal_type(f)
Mypy shows:
Revealed type is "tuple[T`-1, builtins.list[Union[builtins.str, Literal[1], Literal[2]]]]"
Revealed type is "def [T <: builtins.int] (a: T`-1, b: builtins.list[Union[builtins.str, Literal[1], Literal[2]]]) -> Any"
Basedmypy shows:
Revealed type is "(T@f, list[str | 1 | 2])"
Revealed type is "def [T: int] (a: T, b: list[str | 1 | 2]) -> None"
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")
Typed functools.Cache¶
In mypy, functools.cache is unsafe:
@cache
def f(): ...
f(1, 2, 3)  # no error
This is resolved:
@cache
def f(): ...
f(1, 2, 3)  # error: expected no args
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")
Support for typing.type_check_only¶
typing.type_check_only is a decorator that specifies that a value is not available at runtime:
ellipsis  # error: Symbol "ellipsis" is not accessible at runtime  [type-check-only]
function  # error: Symbol "function" is not accessible at runtime  [type-check-only]
Annotations in Functions¶
Basedmypy handles type annotations in function bodies as unevaluated:
def f():
    a: int | str  # no error in python 3.9, this annotation isn't evaluated
Checked Argument Names¶
Basedmypy will warn when subtypes have different keyword arguments:
class A:
    def f(self, a: int): ...
class B(A):
    @override
    def f(self, b: int): ...  # error: Signature of "f" incompatible with supertype "A"
Regex Checks¶
Basedmypy will report invalid regex patterns, and also analyze regex values
to infer the group composition of a resulting Match object:
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¶
<object object at 0x0123456789ABCDEF> and None accidentally appearing in user facing messages is not ideal, so basedmypy will warn against it:
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]