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:

PEP 526

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]