#type-hinting
1 messages Β· Page 42 of 1
if they are all strings then a typevartuple would work, [*Row](names: tuple[*Row], rows: Iterable[tuple[*Row]])
atleast at runtime
yes they must be strings
That works, thank you
Although mypy didn't complain here
from collections.abc import Iterable
from typing import reveal_type
class MarkdownTable[*Row]:
def __init__(self, fieldnames: tuple[*Row], rows: Iterable[tuple[*Row]]) -> None:
self.fieldnames = fieldnames
self.rows = rows
t = MarkdownTable(
fieldnames=("firstname", "lastname", "age"),
rows=(
("Alice", "Smith", "30"),
("Bob", "Johnson", "24"),
("Charlie", "Brown"),
),
)
reveal_type(t.fieldnames)
reveal_type(t.rows)
pyright does complain about the third item
Seems like a bug in mypy
mypy seems to have inferred Row as tuple[str, ...]
I think it's the same case as py def pick[T](first: T, second: T) -> T if I do pick(42, "foo"), what should T be? is it int | str, object, Literal[42, "foo"], or an error?
Yeah I don't think TypeVarTuple is supposed to allow that
isn't error the correct behavior here?
I would assume def pick[T](first: T, second: T) -> T implies that all 3 types must be the same
What if one of the arguments was int and one was a subtype of int?
How so? Seems like it's allowed https://typing.python.org/en/latest/spec/generics.html#splitting-arbitrary-length-tuples
pyright is fine with it too ```py
from collections.abc import Callable
from typing import reveal_type
class Foo[*Ts]:
def init(self, bar: tuple[*Ts]) -> None:
...
def get_callback(self) -> Callable[[*Ts], int]:
...
def f(xs: tuple[str, ...]):
foo = Foo(xs)
reveal_type(foo) # Foo[*tuple[str, ...]]
reveal_type(foo.get_callback()) # (*str) -> int
Yes, you're right
both 42 and "foo" are of the same type -- int | str
That shouldn't be an error I think since it would satisfy def pick(first: int, second: int) -> int
How is that different from picking object?
also object, also Literal[42, "foo"], also Literal[42, "foo", "bar"], the question is how to pick which one
It's the common base type of int and str
right
I didn't even consider unions or the fact that we can keep going till we hit object
Yeah I feel like your suggestions are insufficiently creative. I suggest solving the TypeVar to LiteralString | Literal[42] | TypedDict[{"why": NotRequired[Literal["not"]]}]
Protocol[__str__(self) -> Protocol[__getitem__(self, arg: Literal[0], /) -> Literal["f", "4"]]]
that's more like it
what is that
i am new and knmow only basis can you suggest a course that make me fully advance
But you don't really need to know typing or "become advanced" at it to use Python.
I'm trying to remove Any from a codebase and I have a case where just switching to object doesn't work and want to know how other people solved it or if they just left it as Any.
def extract_coro_stack(
coro: types.CoroutineType[Any, Any, Any], limit: int | None = None
) -> traceback.StackSummary:
...
If I change the Any here to object I can't pass any coroutine to the function as the type is invariant. I don't really care about the parameters, they have no affect on the implementation of the function.
And are there any other cases where Any is considered idiomatic?
Using TypeVars there would work and would prevent any funniness if the function were changed in the future to need to use things bound by the parameters, but creating TypeVars to not use them seems... not great. It'd be nice if there was a type like the default of a TypeVar: all types match like Any, but it has no interface like object.
This is a totally reasonable usage of Any, especially since you're not using anything that's produced by those Anys judging by the function name
I think typevars wouldn't be valid here, since they'd only appear once in the signature
The issue with Any is that if the function changes in a way where it uses the object from the parameters, you have an Any floating around. It's correct now but maybe not in the future, but it will silently keep working in the future, which is one of the big dangers of Any.
Actually, CoroutineType is covariant in the yield and return type, and contravariant in the send type (which makes sense)
https://github.com/python/typeshed/blob/main/stdlib/types.pyi#L438
so you should be able to do types.CoroutineType[object, Never, object]
stdlib/types.pyi line 438
class CoroutineType(Coroutine[_YieldT_co, _SendT_contra, _ReturnT_co]):```
Ah yeah in this case because they are covariant and contravariant I could. Thanks!
I had cooked up a simpler example where the type was invariant and was having trouble with that, but grafted that issue onto my real issue.
yeah for invariant positions it's hard to avoid Any
in cases where you want to accept all specializations
can you make them appear more by using something like a phantom type in the return type?
I'm actually not sure if it's illegal
neither mypy nor pyright complain here ```py
from types import CoroutineType
def f[A, B, C](x: CoroutineType[A, B, C]) -> None:
print(x)
so maybe I'm hallucinating
def f[T](x: T) -> int:
...
warning: TypeVar "T" appears only once in generic function signature
Β Β Use "object" instead (reportInvalidTypeVarUse)
type Phantom[A, B] = A
def f[T](x: T) -> Phantom[int, T]: # fine
...
lol
I'm gonna post it here as well I guess. Apparently it is possible to make something resembling TypeScript's zod that does not require inspecting annotation, even with a spicy partial() transformation. It is very scuffed but it does work (on pyright)
Click here to see this code in our pastebin.
a: Iterator[int] = chain([1, 2, 3], [1, 2, 3])
a: Iterable[int] = chain([1, 2, 3], [1, 2, 3])
is it normal that mypy is ok with both versions?
it should only be Iterator, no?
all iterators are iterables in Python
iter(an_iterator) is an_iterator for all iterators
how can iterator be iterable?
iterable should give you a fresh iterator with iter(), no?
not necessarily
then what's the point of iterable?
that it may do so
and that you can iterate over it
generators, files are some examples of iterators you usually use as iterables.
but list can be iterated over multiple times.
!e This is possible. ```py
xes = ["x", "y"] * 10
for x in (it := iter(xes)):
y = next(it)
print(x, y)
:white_check_mark: Your 3.13 eval job has completed with return code 0.
001 | x y
002 | x y
003 | x y
004 | x y
005 | x y
006 | x y
007 | x y
008 | x y
009 | x y
010 | x y
def foobar(is_bar: bool):
def decorator[**P, T](f: Callable[P, T]) -> Callable[P, T]:
@wraps(f)
def decorated_function(*args: P.args, **kwargs: P.kwargs) -> T:
if is_bar:
pass
else:
pass
return f(*args, **kwargs)
return decorated_function
return decorator
how would i type this?
I tried to type it but I can't get the return type right
foobar returns a function that accepts a function that returns a function.
def foobar(*, is_bar: bool) -> Callable[[Callable[P, T]], Callable[P, T]]: ...
that worked thank you
import typing as t
def format_int(i: t.SupportsInt)->str:
return f"{int(i):,d}"
format_int("100000000") # type error but works at runtime
format_int(100_000_000)
How do I type this function?
stdlib/builtins.pyi line 246
def __new__(cls, x: ConvertibleToInt = ..., /) -> Self: ...```
`stdlib/_typeshed/__init__.pyi` lines 352 to 356
```py
# Anything that can be passed to the int/float constructors
if sys.version_info >= (3, 14):
ConvertibleToInt: TypeAlias = str | ReadableBuffer | SupportsInt | SupportsIndex
else:
ConvertibleToInt: TypeAlias = str | ReadableBuffer | SupportsInt | SupportsIndex | SupportsTrunc```
why do you want this to accept strings though, that seems like a bit of a footgun
I'm typing an untyped project, so I have to type what it does rather than what I want it to be
I would do something like i: int | str if that's what's being passed to the function at runtime
If you want the pedantic annotation, you'll need to replicate this alias
but that seem unnecessary
from typing import reveal_type
from flask_sqlalchemy import SQLAlchemy
from flask_sqlalchemy.model import Model
db = SQLAlchemy()
class BaseModel(Model):
__abstract__ = True
class FooBar(BaseModel):
__tablename__ = "foobar"
query = db.session.query(FooBar)
reveal_type(query)
t.py:15: error: Call to untyped function "query" in typed context [no-untyped-call]
query = db.session.query(FooBar)
^~~~~~~~~~~~~~~~~~~~~~~~
t.py:17: note: Revealed type is "Any"
Why does this fail?
@overload
def query(self, _entity: _EntityType[_O]) -> Query[_O]: ...
_EntityType = Union[
Type[_T], "AliasedClass[_T]", "Mapper[_T]", "AliasedInsp[_T]"
]
For overloads, if the parameter is Any the return type becomes Any. Is Model a subclass of Any?
Oh it isnβt! Not sure then.
If an overload accepts Any, make sure it's defined last.
https://paste.pythondiscord.com/IUXA lines 242 and 259
What error are you getting?
Type "Self@Gaps[T@Gaps]" is not assignable to return type "Gaps[float]"
"Gaps[T@Gaps]*" is not assignable to "Gaps[float]"
Type parameter "T@Gaps" is invariant, but "T@Gaps" is not the same as "float"PylancereportReturnType
and
Argument of type "list[Endpoint[float]]" cannot be assigned to parameter "endpoints" of type "Sequence[T@Gaps | Endpoint[T@Gaps]]" in function "__init__"
"list[Endpoint[float]]" is not assignable to "Sequence[T@Gaps | Endpoint[T@Gaps]]"
Type parameter "_T_co@Sequence" is covariant, but "Endpoint[float]" is not a subtype of "T@Gaps | Endpoint[T@Gaps]"
Type "Endpoint[float]" is not assignable to type "T@Gaps | Endpoint[T@Gaps]"
Type "Endpoint[float]" is not assignable to type "T@Gaps"
"Endpoint[float]" is not assignable to "Endpoint[T@Gaps]"
Type parameter "T@Endpoint" is covariant, but "float" is not a subtype of "T@Gaps"PylancereportArgumentType
Oh I see
consider this ```py
@dataclass
class Apple[T]:
banana: list[T]
@classmethod
def make_default(cls) -> Apple[int]:
return cls([1, 2, 3])
in `make_default`, the type of `cls` is `type[Self@Apple[T@Apple]]`. I'm trying to call that and pass in a list of ints, clearly that's wrong, since `int` isn't necessarily the same as `T`. This can be fixed with a constraint on `cls`py
@classmethod
def make_default(cls: type[Apple[int]]) -> Apple[int]:
return cls([1, 2, 3])
or in your case ```py
@classmethod
def from_string(cls: type[Gaps[float]], gaps: str) -> Gaps[float]:
It's funny that pyright does tell you that the type is type[Self[T]], even though Self[T] is illegal
it's saying "haha, you can't have it, but I can"
Actually, if you want the classmethod to have the right return type on subclasses, it should be ```py
@classmethod
def from_string[S: Gaps[float]](cls: type[S], gaps: str) -> S:
actually... that doesn't seem to work
crap
i had already gave up on lol
seems like you're always going to have Gaps[float] in subclasses
If you don't plan to subclass it, make make it final and add add *bergudgingly* a staticmethod
or a classsmethod that calls Gaps instead of cls
i hate it
welcome to #type-hinting π
ok i think i fixed all the errors, i'm just gonna leave the classmethod alone and returning Gaps[float] not going to worry much about it
Some standard library modules like threading are missing type annotations for some function parameters. Is there a reason for that? Would a PR adding them be welcome?
what is missing annotations in https://github.com/python/typeshed/blob/main/stdlib/threading.pyi ?
Oh, I think I was looking at .py files instead
lib/sqlalchemy/orm/scoping.py line 1627
def query(self, _entity: _EntityType[_O]) -> Query[_O]: ...```
That's not Any. That's a generic
Currently, I am developing Mutli AI system.
I amd detecting fall, fire, violence, and choking
Yes, I meant that the first one should be a match (atleast as far as I understand it) but as you can see from my code example above, query ends up returning Any
What does this have to do with #type-hinting ?
It may be related to flask_sqlalchemy. Try using plain sqlalchemy.
looks sick tho
stdlib/builtins.pyi lines 1891 to 1892
@overload
def sum(iterable: Iterable[bool | _LiteralInteger], /, start: int = 0) -> int: ...```
`stdlib/builtins.pyi` lines 240 to 242
```py
_PositiveInteger: TypeAlias = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
_NegativeInteger: TypeAlias = Literal[-1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16, -17, -18, -19, -20]
_LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed```
babe, you fell asleep
#python-discussion message
What am I missing here?
The mypy Playground is a web service that receives a Python program with type hints, runs mypy inside a sandbox, then returns the output.
from collections.abc import Callable, Sequence
from typing import Literal, Any
type Decorator[**P, T] = Callable[[Callable[P, T]], Callable[P, T]]
def route[**P, T]( # type: ignore[empty-body]
rule: str,
/,
*,
methods: Sequence[Literal["GET", "POST", "PUT", "DELETE", "PATCH"]] = ["GET"],
) -> Decorator[P, T]: ...
@route("blah", methods = ["GET"])
def foo(a: int, b: str) -> bool: ... # type: ignore[empty-body]
reveal_type(foo)
main.py:13: error: Argument 1 has incompatible type "Callable[[int, str], bool]"; expected "Callable[[VarArg(Never), KwArg(Never)], Never]" [arg-type]
main.py:16: note: Revealed type is "def (*Never, **Never) -> Never"
i mean there is technically nothing to bind the typevars from and there are no direct higher ranked types like that, though pyright works with it
i'd make the return type a protocol or something, or implement the decorator as a class
class Decorator(Protocol):
def __call__[**P, T](self, fn: Callable[P, T], /) -> Callable[P, T]:
...
def route(rule: str, *, methods: ...) -> Decorator:
...
# :
class route:
rule: str
methods: ...
def __call__[**P, T](self, fn: Callable[P, T], /) -> Callable[P, T]:
...
I can't alter the implementation, I'm just trying to type it
the protocol should work then
https://mypy-play.net/?mypy=latest&python=3.13&flags=strict&enable-error-code=ignore-without-code&gist=5691f98581bddbc04efc2be685554c0d
The mypy Playground is a web service that receives a Python program with type hints, runs mypy inside a sandbox, then returns the output.
that did the trick, thank you!
Hi, I'm writing models for sqlalchemy. Two different models are in different files, with one-to-many relationship declared between them, in the type hint I write the class name in quotes to avoid cyclic imports. But Pyright gives an error that a model from another module must be imported. How to fix it?
workouts: Mapped[list['WorkoutsModel']] = relationship(back_populates='plan')
Error:
"WorkoutsModel" is not defined"
You need to import the class inside a TYPE_CHECKING block
is this really how typing is done in python?
def query[T](decoded: type[T], *args: str) -> T:
if typing.get_origin(decoded) is list[Struct]:
fields = structs.fields(cast(type, typing.get_args(decoded)[0]))
elif typing.get_origin(decoded) is Struct:
fields = structs.fields(cast(type, decoded))
else:
raise TypeError("Unsupported type passed to query():", decoded)
names = ",".join(field.encode_name for field in fields)
with send_unchecked("query", args[0], names, *args[1:]) as sock:
return json.decode(sock.recv(4096), type=decoded, strict=False)
I am using msgpack
typing.get_origin(decoded) is list[Struct] seems dubious
cast(type, decoded) also seems dubious if decoded is a type[T]
it was the only way I could find to silence pyright errors
how's this
def query[T](response_type: type[T], *args: str) -> T:
if typing.get_origin(response_type) is list:
inner_type = cast(type, typing.get_args(response_type)[0])
if issubclass(inner_type, Struct):
struct_class = inner_type
else:
raise TypeError("Unsupported type:", response_type)
elif issubclass(response_type, Struct):
struct_class = response_type
else:
raise TypeError("Unsupported type:", response_type)
fields = structs.fields(struct_class)
names = ",".join(field.encode_name for field in fields)
with send_unchecked("query", args[0], names, *args[1:]) as sock:
data = sock.recv(4096)
return json.decode(data, type=response_type, strict=False)
handrolling runtime typechecking is probably not the most robust idea
what's the alternative? I'm used to static typing
Hello, I'm struggling to get mypy to stop complaining when I'm overloading an abstract method. Tried searching for a similar issue online but struggled to find similar matches. https://mypy-play.net/?mypy=latest&python=3.12&gist=d2d1963d53e8797f914179ee8e18c5fa
The mypy Playground is a web service that receives a Python program with type hints, runs mypy inside a sandbox, then returns the output.
seems to stop complaining if you add an overload matching the parent signature,
@overload
def m(self, x: int | str | None) -> int | str:
...
pretty weird, but like, average mypy
Great, thanks!
Maybe report that to the mypy issue tracker so one day you can remove the workaround!
Is it a supported use-case for TypeVars to respect "closures" with regard to nested classes?
I've got a use-case where I've essentially got a dispatcher that's calling a function on the values of an iterable, and using a small class to track some metadata about the dispatched tasks. (There's obviously more to it than that, but I don't think the details are relevant. Feel free to ask, though, if it'd be useful.)
The class for the dispatched tasks is tiny, and won't get used anywhere else, so I'd ideally like to nest it within the class for the dispatcher. Also, I want to "bind together" the input/output types of the callable, the iterable, and the dispatched tasks so that a caller knows what types of output values will be produced (i.e. the return type from the callable).
I thought I could do this by using the same TypeVar instances between the outer and inner classes, where the inner class's TypeVars would "inherit the meaning of" the scope of the outer class. However, it doesn't seem like that actually works:
class Dispatcher(Generic[InputType, ReturnType]):
fn_to_call: Callable[[InputType], ReturnType]
iterable: Iterable[InputType]
# And then a bunch of other stuff, not relevant here
# Error: "TypeVar _ is already in use by an outer scope"
class DispatchedTask(Generic[InputType, ReturnType]):
input_value: InputType
output_value: ReturnType
# And then a bunch of other stuff, not relevant here
# No typing errors when DispatchedTask isn't a nested class
class DispatchedTask(Generic[InputType, ReturnType]):
input_value: InputType
output_value: ReturnType
Should this actually be working, and I'm just doing something wrong, or is this just not supported? (I'm using Pyright via Pylance v2025.6.1 in VS Code.)
(I can see how to get this to work by either using a different TypeVar name or just un-nesting the class, but I really thought this would work, and I'm curious about why not.)
iirc generic inner classes are a bit buggy, see thread starting from #type-hinting message
I would probably just have it as a non-inner class
Generic type alias within class cannot use bound type variables lame
Ah, well. Fair enough, and thanks for the link
A dumb question - what type aliases are needed for, and why I shouldn't just define Vector = list[float] instead of Vector : TypeAlias = list[float]?
Is there any cases when adding TypeAlias is critical and things would work differently if it's not added?
The only benefit I've found is Vector: TypeAlias = "list[int]" allows creating a type alias without evaluating list[int] at runtime.
But then if someone will miss TypeAlias nature of a variable, they might run into 'str' is not callable at runtime for Vector([1,2,3]), though there are no typing errors.
For me, the TypeAlias type hint is always just there for clarity, never for any functional reason
Especially in contexts like setting up a type alias within a class, where it typically goes up near the top along with all of the other class-attribute type hints; doing foo: TypeAlias = bar instead of just foo = bar makes it clear that this is just for type-aliasing purposes and isn't supposed to be a class attribute or a normal sort of class constant
I haven't read through type keyword PEP or it's discussion, but it's kind of surprising that breaking syntax change was introduced just for some code clarity π
What broke?
I meant it's not backwards compatible
In what way?
# SyntaxError: invalid syntax
type test = int
Isn't this the new syntax that is available since 3.12?
Indeed
Use this instead. ```py
from typing import TypeAlias
test: TypeAlias = int
if you need to use 3.10 union syntax on 3.9, wrap it in a string
Won't work at runtime though
If you string wrapped type alias has a generic, its usage much also be wrapped in string
or add from __future__ import annotations
Who cares about runtime?
Libraries that do runtime checking?
I understand, I'm just missing the point of explicitly specifying type aliases in general and therefore I don't get the need for a special keyword type for it
I was under the impression that type checkers would disallow using a type alias in a runtime context, but apparently I'm wrong
yeah it seems like this is the main use case for it
TypeAlias is particularly useful on older Python versions for annotating aliases that make use of forward references, as it can be hard for type checkers to distinguish these from normal variable assignments:
breaking change means that old code doesn't work
I guess. If I previously used type banana = Fruit to cause a syntax error, I no longer get that effect.
So, not 100% backwards compatible? π
its nice for defining generic aliases, as you get to see the typevars right in the definition
3.14 version is the most backwards incompatible one, because it will break this code which worked perfectly before: ```py
import sys
if sys.version_info >= (3, 14):
raise SystemExit(42)
Okay, I've meant forward compatibility, thanks for making fun of me
A bit late for that correction init
How do I create a generic class with a type parameter bound to the same generic class, whose default value is Self? Ideally, I'd like to write something like this:
from abc import abstractmethod
from typing import Self
class C[T: C = Self]: # This is the issue
@abstractmethod
def __matmul__(self, other: Self | T) -> Self:
pass
For context as to why I would want this: C represents a generic class of mathematical functions that can be composed, using @, (technically a magma), T represents an additional class that is also allowable as input for composition.
I've tried using TypeVar, aliases, strings but always seem to run into some issue and can't remove the cyclic definition errors.
The issue is that this class is impossible to instantiate properly I think?
No ok I got it twisted, typevar default would make it fine...
But the typevar can never be bound otherwise then π€
Is there some relevant code you didn't share?
There's more to it, but I've stripped it down to the nub of (this particular) issue. The class can't be instantiated as it's an abstract class, but subclasses override the abstract methods. For example, I have AffineFunction and PiecewiseAffineFunction subclasses, where the latter would (ideally) override T's value with AffineFunction.
How would you get the default to work? I can't use default=Self in TypeVar either
Self itself is kind of a typevar bound by implicit self: Self in normal methods or cls: type[Self] in classmethods, and you cant specify a typevar as a typevar default, so uh yeah thats why what you're trying doesn't work
if the only place you use it in is other: Self | T, the default for T could be Never, so it'd kinda cancel out to other: Self, but allow you to override it in subclasses to another type
Great, that fixes it! I was a bit concerned if it would result in Never showing up in help output, but it always shows the type parameter name instead for methods inherited from C.
the = you were doing in the type parameter is already the same thing π
but hey you found a solution
pretty much every major python version is not backwards compatible
Missing: TypeAlias = Union[UnsetType, T, None]
Missing[str] # equivalent to Union[UnsetType, str, None]
This blew my mind, why is this possible and which part of the docs say that this is possible? I didn't know Union's also behaved like generics if they had TypeVars in them
That's not a feature of Union, that's a feature of type aliases. You created a "generic type alias"
https://typing.python.org/en/latest/spec/aliases.html
You could also do e.g. py ListOrTuple: TypeAlias = list[T] | tuple[T, ...] and then use ListOrTuple[str]
thats very cool, thanks
hello
T = TypeVar("T", decimal, float)
does that mean decimal and float are restricted??
it means that T can only represent those exact types, and not subtypes
so if you pass a subtype in, it will be treated as the parent
from typing import TypeVar
T = TypeVar("T", int, str)
def f(x: T) -> T:
return x
x: bool = True
reveal_type(f(x)) # int, even tho we specifically passed in a bool
Thanks bud
Is it normal that this does not raise any issue with Mypy strict? Doesn't it violate the Liskob Substitution principle?
from __future__ import annotations
from abc import abstractmethod
from typing_extensions import override
class A:
@abstractmethod
def __init__(self, label: str) -> None:
pass
class B(A):
@override
def __init__(self) -> None:
pass
I also tried with and without (abtstractmethod and ABC). All possible combinations
Apparently it raises an error only if I rename the init to anything else
I believe LSP only applies to instance methods
That's terrible in my opinion ππ»ββοΈ
Typecheckers specifically don't flag a differing __init__ signature. They do check all *other class and instance methods otherwise if you set the right settings (reportIncompatibleMethodOverride in pyright for example)
There's a post on discourse on this https://discuss.python.org/t/enforcing-init-signature-when-implementing-it-as-an-abstractmethod/75690/6
But there is not anything for Mypy, right?
neither of them support enforcing __init__ compatibility
sorry, I meant that they should check all other class and instance methods
not just instance methods as Grote said
Is it considered bad practice to have an optional argument with type hint e.g. zoom: int = None? zoom: int | None = None feels unnecessarily verbose
x: int | None = None is very common
But remember if x changes the behavior of the function, it's probably best to make a new function instead.
Wouldn't having many composed functions also add to cognitive overhead?
Not sure what you mean.
You're saying it's better to compose with an additional function instead of having an optional argument that could be None
I'm asking what's the point of the optional argument?
python has optional arguments for a reason, but don't use them to give one function the duty of two.
zoom: int = None is incorrect, zoom: int | None = None would be correct
https://peps.python.org/pep-0484/#union-types
A past version of this PEP allowed type checkers to assume an optional type when the default value is None, as in this code:
def handle_employee(e: Employee = None): ...
This would have been treated as equivalent to:
def handle_employee(e: Optional[Employee] = None) -> None: ...
This is no longer the recommended behavior. Type checkers should move towards requiring the optional type to be made explicit.
this is a better source probably https://typing.python.org/en/latest/spec/concepts.html#union-with-none
Am I missing something or is the interaction between typing and coroutines/asyncio, pretty bad?
hmm maybe I'm missing some package with typing stubs
async def count() -> int:
print("One")
await asyncio.sleep(1)
print("Two")
return 1
async def blub():
y = [x.result() async for x in asyncio.as_completed(count() for _ in range(3))]
given this, when I hover over y pylance just says it's a list
hmm maybe it's the use of async for? mypy doesn't seem to like that
though it works just fine
same with pylance
I'd expect that to work, if it doesn't that's a bug
yeah, if I change it to a normal for it works
The mypy Playground is a web service that receives a Python program with type hints, runs mypy inside a sandbox, then returns the output.
looks like as_completed supports async for only in 3.13
ahhhh got it
thanks for that
so, what is the proper way to annotate my count function itself? is it really Coroutine[None, int, None] ?
actually, sorry
[None, None, int]
I'm surprised there isn't some kind of convenience alias I guess
int is right as the return type
but maybe Any instead of None?
I guess we always put Any for the first two type params to Coroutine, I've never invested the time to figure out exactly what they need to be
I think it's mostly relevant to the internals of the async framework
it's the send type and yield type, I suppose reflective of the fact that you have these 3 features of coroutines, and you could use all 3
On an async function, py async def count() -> int: means the type of count is Callable[[], Coroutine[Any, Any, int]]
It jsut feels like in practice, 99% of the time people are not going to be using all 3 of those types simultaneously.
And a lot of the time they'll just be writing Coroutine[Any, Any, Whatever]
If you annotate the return type as Coroutine[Any, Any, int], it's going to become Callable[[], Coroutine[Any, Any, Coroutine[Any, Any, int]]] which is wrong
yes, you're right, my bad
if you're using async, the sending and yielding will be done by the async framework you're using (asyncio/trio or something else)
you can also use Awaitable[int] which is often interchangeable with Coroutine[Any, Any, int]
though there are some operations for which you need a Coroutine and not an arbitrary Awaitable
yeah, typing basically says not to use it in most cases though
i think for the average person Awaitable is going to typically be a trap because it's not at all obvious when you need Coroutine, and Awaitable may not be very interesting in the sense of admitting a lot more types
...so these parameters could be useful if you want to avoid mixing up async functions for different loops. asyncio could export asyncio.Coroutine which would be like type asyncio.Coroutine[T] = Coroutine[asyncio.Future, Any, T]. But that's never going to happen, probably
in particular because async functions don't let you annotate those two parameters, and it would be an enormous breaking change if they required an explicit Coroutine type
yeah I was just thinking a convenience alias
_T = TypeVar("_T")
AsyncFunc = Coroutine[Any, Any, _T]
async def run_all(coroutines: Iterable[AsyncFunc[_T]]) -> list[Task[_T]]:
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(c) for c in coroutines]
return tasks
I think i"ll probably just be using this simple wrapper a lot
Which works well vis-a-vis typing
actually, going through the docs, I found this
Important In this documentation the term βcoroutineβ can be used for two closely related concepts:
a coroutine function: an async def function;
a coroutine object: an object returned by calling a coroutine function.
so a better annotation would probably be CoroutineObject or AsyncObject for what I had above
I have the following code which I'd like to have type hinted correctly... I'm not super confident with how Concatenate and ParamSpec's work which I think is why I'm getting stuck:
from ctypes import Structure
from collections.abc import Callable
from typing_extensions import Concatenate, Generic, ParamSpec, TypeVar
from typing import Any, Optional
P = ParamSpec("P")
R = TypeVar("R")
class FunctionHook(Generic[P, R]):
...
class function_hook():
def __init__(self, sig: Optional[str] = None):
self.sig = sig
def __call__(self, func: Callable[Concatenate[Structure, Any, P], R]) -> FunctionHook[P, R]:
...
class Thing(Structure):
@function_hook("test")
def my_func(self, arg1, arg2):
pass
The issue is with the function_hook decorator...
You're missing arg2
The error provides a hint: Structure is not assignable to Thing.
When you have a method inside a class, the first argument (self) doesn't require a type annotation and is assumed to have the type of Self, which must be at the very least an instance of your current class (Thing). So you can't treat it as a function that accepts any Structure -- i.e. it would be valid to do this in function_hook.__call__ ```py
class DefinitelyNotThing(Structure):
...
class function_hook:
def call(self, func: Callable[Concatenate[Structure, Any, P], R]) -> FunctionHook[P, R]:
# somewhere inside of FunctionHook:
func(DefinitelyNotThing(), 42, *args, **kwargs)
So you either need to annotate `self` to be any Structure (probably a bad idea but maybe it works in your case) ```py
@function_hook("test")
def my_func(self: Structure, arg1, arg2):
pass
``` or introduce an extra typevar, which might need to be propagated to `FunctionHook` as well: ```py
S = TypeVar("S", bound=Structure)
class function_hook():
def __call__(self, func: Callable[Concatenate[S, Any, P], R]) -> FunctionHook[P, R]:
# -> FunctionHook[S, P, R] probably
...
Ah, the S = TypeVar("S", bound=Structure) works perfectly, thanks!
I know the error says "Structure is not assignable to Thing" but this confused me because Thing is a subclass of Structure, so the error doesn't seem clear to me (at least how I am (mis)understanding it)
but thanks for clearing this up
Functions are contravariant in their parameters. It means that e.g. since bool is a subtype of int, Callable[[int], str] is a subtype of Callable[[bool], str], but not the other way around.
ah ok thanks, that makes sense!
Ok, actually got another one I'm curious about...
So I have a decorator like so:
def on_key_release(event: str):
def wrapped(func: Callable[..., Any]) -> KeyPressProtocol:
func._hotkey = event
func._hotkey_press = "up"
return func
return wrapped
Where
class KeyPressProtocol(Protocol):
_hotkey: str
_hotkey_press: str
I get a type error that _hotkey is not present when I hover over return wrapped. It goes away if I type func as KeyPressProtocol, but is that correct to do so?
This is a good use for # type: ignore
Ok, and maybe last one for now...
Is it possible to indicate to a type checker that a function is static without using the @staticmethod decorator?
Similar to the above, I have another decorator which looks like:
class static_function_hook():
def __init__(self, sig: Optional[str] = None):
self.sig = sig
def __call__(self, func: Callable[P, R]) -> FunctionHook[P, R]:
...
If I have the following:
class Thing(Structure):
@static_function_hook("another")
def my_static(arg1: int, arg2: float):
pass
It complains (rightfully so!) that the type of arg1 isn't right...
I think I could add @staticmethod as the first decorator, but just wondering if there is another way as this decorator will always be applied to static methods...
You must add @staticmethod directly on the function
Damn, ok. Was hoping I might be able to like... Make my own decorator which acts as a staticmethod decorator while adding some extra functionality
Hi,
I get those errors in my CI https://github.com/moi15moi/VideoTimestamps/actions/runs/15890903217/job/44813130877?pr=14#logs:
video_timestamps/video_timestamps.py:6: error: Module "video_timestamps.video_provider" does not explicitly export attribute "ABCVideoProvider" [attr-defined]
video_timestamps/video_timestamps.py:6: error: Module "video_timestamps.video_provider" does not explicitly export attribute "FFMS2VideoProvider" [attr-defined]
I don't understand why it happen.
So, in video_timestamps/video_timestamps.py, I import the video_provider module which import ABCVideoProvider and FFMS2VideoProvider.
Note that both of these class are defined with pybind11 (in c++), so I have created .pyi files here, but mypy seems to ignore them?
you need to explicitly export them in video_timestamps/video_provider/__init__.py
from .abc_video_provider import ABCVideoProvider
from .best_source_video_provider import BestSourceVideoProvider
from .ffms2_video_provider import FFMS2VideoProvider
__all__ = ["ABCVideoProvider", "BestSourceVideoProvider", "FFMS2VideoProvider"]
No, Python's type system does not have negations
Your fix does solve the issue, but it's weird, no?
In my video_timestamps/__init__.py, I didn't need to specify them in the __all__. Why, suddenly, I would need to?
Hello!
I'm trying to type hint a custom "container" class.
It's basically a dictionary, and you can add things to it by a key, and you can get things from it using a key.
Is it possible in python to type hint it so that the key passed to the add_your_thing function and the get_your_thing must be of the "Mapping" type the user defined?
Here's some pseudo-code to better show what I mean:
class SomeContainer[YourMappingType: dict[str, Any]]:
_data: YourMappingType
def __init__(self):
self._data = {}
def add_your_thing(self, key: MustBeAKeyOf[YourMappingType], value: YourMappingType[key]):
self._data[key] = value
def get_your_thing(self, key: MustBeAKeyOf[YourMappingType]) -> YourMappingType[key] | None:
return self._data.get(key)
Would appreciate any help!
Thanks π
why not SomeContainer[K, V]? and when would the key not be a string given that its bound by a dict with str keys?
But I want the type system to understand that if I do something like:
class Person:
pass
container = SomeContainer()
container.add('person', Person())
p = container.get('person')
then p can only be of type Person
in this exact case thats impossible with current typesystem and typecheckers because the type variable is resolved on creation, and in container = SomeContainer() there is nothing to resolve it from
and if you want each key to have a specific type - thats not possible with a custom class, only TypedDict gets that because its special for whatever reason
why not define classes for structures with knows keys?
I see, thats what I was assuming. Thanks for confirming it. Kind of a bummer though!
Well, I was trying to do a generic thing where the user of this container would define the "mapping" of each key to each type using a generic somehow
from __future__ import annotations
from typing import Any, LiteralString, Literal, reveal_type
type HasHead[Name: LiteralString, Field] = Container[Name, Field, Any]
type HasTail[Name: LiteralString, Field] = Container[Any, Any, HasField[Name, Field]]
type HasField[Name: LiteralString, Field] = HasHead[Name, Field] | HasTail[Name, Field]
class Hack[Name: LiteralString, FixContainer]:
def __call__[Field](self: Hack[Name, HasField[Name, Field]]) -> Field: ...
class Container[HeadName: LiteralString, HeadField, Tail = None]:
def add[Name: LiteralString, Field](self, name: Name, field: Field) -> Container[Name, Field, Container[HeadName, HeadField, Tail]]:
...
def __getattr__[Name: LiteralString](self, name: Name) -> Hack[Name, Container[HeadName, HeadField, Tail]]: ...
def test(container:
Container[Literal["x"], int,
Container[Literal["y"], str,
]]) -> None:
reveal_type(container.x()) # int
reveal_type(container.y()) # str
reveal_type(container.z()) # error, as expected
modified = container.add("z", 3.5)
reveal_type(modified.x()) # int
reveal_type(modified.y()) # str
reveal_type(modified.z()) # float
(thanks to fix-error for inspiration)
What about this? ```py
class TypeMap:
def init(self) -> None:
self._data: dict[Any, Any] = {}
def put[T](self, ty: type[T], value: T) -> None:
self._data[ty] = value
def get[T](self, ty: type[T]) -> T | None:
return self._data.get(ty)
store = TypeMap()
store.put(Person, Person())
Alternatively (what aiohttp does for example, it's more flexible because it allows different values of the same type) ```py
class Key[T]:
def init(self, name: str) -> None: ...
CEO_KEY = KeyPerson
CTO_KEY = KeyPerson
class Map:
def put[T](self, key: Key[T], value: T) -> None: ...
def get[T](self, key: Key[T]) -> T: ...
store = Map()
store.put(CEO_KEY, Person("Alice"))
store.put(CTO_KEY, Person("Bob"))
Technically you don't need to init with a name, it would only be used in the repr
well then you'd be left relying on identity for lookups
That makes sure you can't initialize the same key with a different generic
Yes, it just helps with debugging (and yes, the keys will be used by identity)
hi
challenge, make this type-safe
class StringBuilder:
def commasep[T](
self,
items: list[T],
callback: Callable[..., None],
pass_self: bool = False,
*args: Any,
**kwargs: Any,
) -> None:
for i, item in enumerate(items):
if pass_self:
callback(self, item, *args, **kwargs)
else:
callback(item, *args, **kwargs)
if i != len(items) - 1:
self.print(", ")
overload over pass_self Literal False and Literal True, use a paramspec for args and kwargs, use typing.Self or an explicit typevar for self
its also being subclassed
so callback self param should know about the subclass
how would i do that?
you can typevar on self
why are you using variadic arguments if there is a specific signature?
there isn't a specific signature
only thing is that the first argument may consume self
so why did you say this?
from collections.abc import Callable, Sequence
from typing import Concatenate, no_type_check, overload, Literal
class StringBuilder:
@overload
def commasep[T, **P](
self,
items: Sequence[T],
callback: Callable[Concatenate[T, P], None],
pass_self: Literal[False] = False,
*args: P.args,
**kwargs: P.kwargs,
) -> None: ...
@overload
def commasep[Self, T, **P](
self: Self,
items: Sequence[T],
callback: Callable[Concatenate[Self, T, P], None],
pass_self: Literal[True],
*args: P.args,
**kwargs: P.kwargs
) -> None: ...
@no_type_check # why is this not the default in overload implementations? idk. its not like you can actually give a correct hint to commasep in here, thats why we're using overload in the first place
def commasep(
self,
items,
callback,
pass_self = False,
*args,
**kwargs
) -> None:
for i, item in enumerate(items):
if pass_self:
callback(self, item, *args, **kwargs)
else:
callback(item, *args, **kwargs)
if i != len(items) - 1:
self.print(", ")
(this doesnt really need to be a sequence tbh, iterable could work with a bit of manual next'ing and handling stopiteration)
and you can do this concatenate thing without concatenate btw, its not really special here
class CallbackNoSelf[T, **P](Protocol):
def __call__(self, x0: T, /, *args: P.args, **kwargs: P.kwargs) -> None: ...
class CallbackWithSelf[Self, T, **P](Protocol):
def __call__(self, x0: Self, x1: T, /, *args: P.args, **kwargs: P.kwargs) -> None: ...
tbh im not a fan of argument passthrough like that, i'd rather just pass lambda: callback(*args, **kwargs)
makes all your code littered with these paramspecs
why the @no_type_check? I'd do; ```python
def commasep(
self,
items: Sequence[object],
callback: Callable[..., None],
pass_self: bool = False,
*args: object,
**kwargs: object,
) -> None:
this is a pretty useless signature, the body wont be typechecked properly
it's not
the reason for using overload in the first place is you cant give a correct signature
I mean, not in the generic sense of calling callback
but it does give some value
what value?
well, without it, you wouldn't even know that items is a sequence, or that pass_self is a bool inside of the func body, no?
pyright would just treat them all as unknown
with @no_type_check, its basically like slapping # type: ignore on the scope of the function, it wont care. the signatures are given by overloads.
yeah, but that's not good
no, thats just overloads being stupid in python.
you get no internal type checking
why not have the body of the func type checked
even if it's not as strongly type-checked
it's still beneficial imo
i disagree.
well, that's obv up to you, but I like the body of my overloaded functions to be type checked, just like with any other funcs, in this case, it is a small body sure, but still, it gives you the same benefit that type checking would give you for any other code
no, its not the same benefit at all. it prevents from very trivial mistakes (which are avoided by reading the names of parameters), it does not actually enforce correct relationships between the types of arguments.
why loose up on checking that items is used properly as a list?
if you were to use some specific functions to sequence inside of the func body, you'd get that validated, why give up on it?
"if i were..."
I mean, even len is technically one
i dont care about annotating the implementation of overloaded functions because in most cases its not useful. i dont even think about it at this point.
@overload
def f(x: X1, y: Y1) -> Z1: ...
@overload
def f(x: X2, y: Y2) -> Z2: ...
def f(x: X1 | X2, y: Y1 | Y2) -> Z1 | Z2: ... # ah yes such useful, amazing! (no)
I've never seen someone have that stance, so I find it pretty interesting, I mean, if you like type-checking in general, I'd imagine you'd want it everywhere
well yeah, at least you won't return a Z3
or won't call something that neither X1 nor X2 has
I do see a benefit there
like sure, if you're just defining signatures, it doesn't matter
practically, i write typehints in python for autocompletion.
but if that func has a body, and you care about type-checking your code, then that does matter
even then it matters, you get completion within the function body of the overload
the typechecker wont like you using x.f2() until you narrow x: X1 | X2 to X2 (which is usually implied by the overload context, but the typechecker doesnt get it)
okay, I get that, I just throw casts for that into the code
why should i write more code than needed? im sure the implementation is correct, the typechecker just doesnt get it.
I just prefer to have it statically validated that it is indeed correct
(in practice, this gets even worse, much worse)
i'd have to write casts everywhere, and i dont want to.
especially when the function body of the overload is larger
i just split it into separate functions at this point, which can be typechecked fine. i use overload for things that can be trivially implemented but have to be overloaded.
it's usually not that bad honestly, I mean, if you do multiple bool flags that control types per function, probably just split it into multiple functions
and for a single flag, it's ususally managable
or use a higher type, if you don't need specific things and your impl works in general with just those, which it often actually does, like in the case with this one, no casts were even necessary
as i said, i dont even think about the specific case anymore as i've spent too much time proving that my overload implementations are "correct" to stupid typecheckers because python overloads suck.
fair enough
i dont think its possible
thank you so much
what? the overloads are correct
i didn't read your msg
i concluded from my trials
i forgot about concatenate
@overload
def commasep[T, **P](
self,
items: Sequence[T],
callback: Callable[Concatenate[T, P], None],
pass_self: Literal[False] = False,
*args: P.args,
**kwargs: P.kwargs,
) -> None: ...
@overload
def commasep[Self, T, **P](
self: Self,
items: Sequence[T],
callback: Callable[Concatenate[Self, T, P], None],
pass_self: Literal[True],
*args: P.args,
**kwargs: P.kwargs,
) -> None: ...
def commasep[T](
self,
items: Sequence[T],
callback: Callable[..., None],
pass_self: bool = False,
*args: Any,
**kwargs: Any,
) -> None:
for i, item in enumerate(items):
if pass_self:
callback(self, item, *args, **kwargs)
else:
callback(item, *args, **kwargs)
if i != len(items) - 1:
self.print(", ")
atleast it makes it more type-safe inside the definition
yeah, I agree
the generic [T] in the implementation doesn't give you much though
that's why I suggested just Sequence[object]
Any will let you do items[0].somethingthatmightnotexist()
without reporting a problem
:white_check_mark: Your 3.13 eval job has completed with return code 0.
True
I always tend to prefer object if the type can be anything arbitrary, unless there's a good reason for Any, like that you want to be able to make assumptions without casting
from abc import ABC
from pydantic import BaseModel
from typing import ClassVar
class Foo[E: BaseModel](ABC):
extras_cls: type[E]
def __init_subclass__(cls, extras_cls: type[E]) -> None:
cls.extras_cls = extras_cls
return super().__init_subclass__()
def extras(self) -> E:
...
class Extras(BaseModel):
...
class Bar(Foo, extras_cls=Extras):
pass
b = Bar()
reveal_type(b.extras())
``` hi there, pylance thinks b.extras() here returns Unknown, I would like it to be Extras
is there a way I can use this method of using init subclass and having the typing be correct, also noticed I cant use ClassVar with type[E] since it doesnt allow type inside of ClassVar, which is fine if I can do it another way
of course I can use py class Bar(Foo[Extras], extras_cls=Extras): but that requires me to type it twice, heh
You're missing a typevar on Foo
If you omit a typevar, it implies Any, which is resolved to the bound type, BaseModel
strict mode will catch this.
maybe pyright standard too
is E not interpreted as a typevar with the new generic syntax
class Bar(Foo[Extras], extras_cls=Extras): pass
btw, you can access Extras without extras_cls by using cls.__orig_bases__ I think
Yeah I know thats possible but that requires me to type Extras twice which is what Im trying to avoid, I think this should be possible, no? It would see that Extras is used as E for the __init_subclass__ and use that as generic
huh how would this work? What would cls be?
class Foo[E: BaseModel]:
def __init_subclass__(cls):
if hasattr(cls, "_extras_cls"):
return
for base in cls.__orig_bases__:
if typing.get_origin(base) is Foo:
cls._extras_cls, = typing.get_args(base)
return
class Bar(Foo[Extras]):
pass
ah that could work
This information should still be available via type(b)
i once had a project called runtime_generics but it didn't use orig_bases (see issue 26 xd)
https://github.com/bswck/runtime_generics
!d types.get_original_bases is in 3.12
types.get_original_bases(cls, /)```
Return the tuple of objects originally given as the bases of *cls* before the [`__mro_entries__()`](https://docs.python.org/3/reference/datamodel.html#object.__mro_entries__) method has been called on any bases (following the mechanisms laid out in [**PEP 560**](https://peps.python.org/pep-0560/)). This is useful for introspecting [Generics](https://docs.python.org/3/library/typing.html#user-defined-generics).
For classes that have an `__orig_bases__` attribute, this function returns the value of `cls.__orig_bases__`. For classes without the `__orig_bases__` attribute, [`cls.__bases__`](https://docs.python.org/3/reference/datamodel.html#type.__bases__) is returned.
Examples:
was very fun to do, and very hacky :-)
fun fact: doing class X(Base[Y]) is a common way to reify generics.
Is there any way to type metaclass parameters or whatever they're called?
class MyClass(MetaClassSubclass, option1=..., option2=...):
does it have to be a metaclass? because typehints on __init_subclass__ parameters "just work"
Hmmmm
Is it intended that Pylance shows hint for __init__ on hover while if you type in the brackets it shows hint from __new__?
class Base[T]:
def __new__(cls) -> T:
return super().__new__(cls, *args, **kwargs) # type: ignore
def __init__(self, target: T) -> None:
self._target: T = target
class Der(Base[int]):
def __init__(self, target: int) -> None:
super().__init__(target)
a = Der()
What I mean is this
idk, but it shows this anyway
Mismatch between signature of __new__ and __init__ in class "Der"
Signature of __init__ is "(target: int) -> None"
Signature of __new__ is "() -> int"PylancereportInconsistentConstructor
But is it possible to have different signature for new and init without overriding new in the child class?
T strings would be pretty useful for building typing literals I think.
type EventName = Literal[t"on_{str}"]
Would it be possible to override init arguments without overriding new?
~~I feel like whatever you want isn't related to typing? #python-discussion / #1035199133436354600 ~~ seems like it is when reading the context above
Yeah... the thing I would like to have is a proxy object (encapsulate an arbitrary object, in my case numpy.random.Generator and forward a set of methods to it).
And the code above was the closest I could get to it, but there are some typing issues I encountered and have no idea if it is even possible to resolve.
Also not sure if Pylance should show different signatures there (or am I wrong?).
Please react with β
to upload your file(s) to our paste bin, which is more accessible for some users.
Please react with β
to upload your file(s) to our paste bin, which is more accessible for some users.
i have an example code like this
from typing import TypedDict
user = dict(name="Name", age=100)
print(user)
items = user.items()
keys = user.keys()
values = user.values()
class User(TypedDict):
name: str
age: int
typed_user = User(name="Name", age=100)
print(typed_user)
typed_items = typed_user.items()
typed_keys = typed_user.keys()
typed_values = typed_user.values()
and items, keys, values are being typed as dict_items[str, str | int], dict_keys[str, str | int] and dict_values[str, str | int] by type checker
while typed_items, typed_keys, typed_values are being typed as dict_items[str, object], dict_keys[str, object] and dict_values[str, object]
Why is it that the dict one is being typed better than TypedDict and what could be changed to have it being typed better?
stdlib/_typeshed/_type_checker_internals.pyi lines 40 to 42
def items(self) -> dict_items[str, object]: ...
def keys(self) -> dict_keys[str, object]: ...
def values(self) -> dict_values[str, object]: ...```
TypedDicts may have additional keys not reflected in the type
class Foo(TypedDict):
x: str
class Bar(TypedDict):
x: str
y: int
bar: Bar = {"x": "a", "y": 1}
foo: Foo = bar # ok
``` it's only disallowed when assigning a literal, which can be a bit confusing
You can use closed=True from PEP 728
Will that also improve type safety / type inference?
It will make some TypedDicts behave differently
def record(func: Callable[..., Any]) -> type: ...
@record
def user(name: str, age: int) -> None:
pass
foo: user = {
"name": "Alice",
"age": 30,
}
print(foo)
is it possible to type hint this?
Use a ParamSpec
how will you make it into a TypedDict?
Oh, you want it to be a typed dict.
converting a function spec to a TypedDict isn't supported.
i actually want it to be a named tuple
But the other way around is possible.
or dataclass
from typing import NamedTuple
class User(NamedTuple):
name: str
age: int
foo = User("Harry", 2)
print(foo)
from dataclasses improt dataclass
@dataclass
class User:
name: str
age: int
from typing import TypedDict
class User(TypedDict):
name: str
age: int
foo: User = {
"name": "Alice",
"age": 30,
}
def function_that_accepts_user_args(*args: Unpack[User_namedtuple]):
print(args[0], args[1])
def function_that_accepts_user_kargs(**kwargs: Unpack[User_typeddict]):
print(kwargs['name'], kwargs['age'])
I can't remember if Unpack supports named tuple or not.
it's probably just a tuple[str, int] type
It does support namedtuple. Oddly, reveal_type on args shows ```py
tuple[builtins.str, builtins.int, fallback=main.User_namedtuple]
Not sure what the fallback is for
ok this is probably a stupid question, but why is there a whole channel dedicated to an optional feature in python
It's a popular feature
is variance determined by typecheckers or specified in python?
and following that, how do i check the variance of parameters in generic types
looking specifically for collections.abc.Generator
Both. We've got two types of syntax for typevars, old and new. The old typing.TypeVar() syntax has explicit parameters to indicate variance, while the new dedicated class Blah[T] syntax infers it. But the inference rules are anyway a good way you can identify what variance is allowed: If a variable is present in a return type or on a readable attribute, it can be covariant. If the variable is a parameter or on a writable attribute, it can be contravariant. If both, it has to be invariant.
As to how to identify generic type variances, you can follow that rule, or in most cases look at the definition - by convention typevars have _co or _contra to indicate variance. For Generator, Yield and Return are covariant, while Send is contravariant.
The spec has a more precise algorithm for variance inference, but it's quite a bit more technical.
π thanks!
for another question
if i have
class Foo[T](Protocol)
def foo() -> T: ...
class Bar(Foo[Sequence[int]]):
@override
def foo() -> Sequence[int]: ...
is there any way to avoid the duplicate Sequence[int]? basedpyright doesn't seem to infer it if i leave it off
There isn't currently no, according to the spec etc. Unless basedpyright has an option for it.
There are reasons for this - it means when looking at the method body you don't have to look up the caller to check the types, and it protects against an inadvertent change in the base class later since the child would then immediately error. But it can be repetitive.
@oblique urchin Are you accepting contributions to https://github.com/JelleZijlstra/unsoundness? I have several examples in mind that don't involve Any, typing.cast, # type: ignore, TypeGuard or TypeIs
Yes! I have a few more examples to add myself too, but feel free to make contributions
Also planning to add a bit of a testing framework that validates in CI that the examples fit the criteria
Was going to advertise it a bit more after that's done, but feel free to contribute now
btw there's a typo in the repo description
Collecting examples of unsoundness in the Pythone type system
thanks, fixed
Would an example be valid if it only works with one type checker? Or is that for nonexamples?
Ideally not, unless you can make the argument that only the type checker where it works is following the spec
I did just add that nonexamples folder. I guess I could accept more there but I'm wary of this turning into an alternative bug tracker for those type checkers
You know what I'll just document the nonexamples folder and organize it a bit. I can always drop it if it turns out too noisy
I think it's useful to document unsoundness instances that type checkers agreed are not worth fixing, i.e. are by design
Seems like github has lost its mind https://github.com/JelleZijlstra/unsoundness/pull/6/files it thinks that I added files that are already present in the repo. Is that because of rebasing? Should I just... recreate the PR?
Maybe rebasing, yes. I'd suggest cleaning up your branch locally (something like git fetch origin; git rebase origin/main) and force-pushing the branch
nice example though, I'll merge it once the branch is clean
put two small comments on GH
well well well, my UPS failed and now I have a buggy repo
(jelle-unsoundness) [df@duckpond jelle-unsoundness]$ git stash
BUG: diff-lib.c:622: run_diff_index must be passed exactly one tree
Aborted (core dumped)
finding unsoundness not just in Python but in git, all for the price of one
not sure if git is going to accept this as a bug
presumably some filesystem nodes failed to sync to the disk due to my UPS failure
interesting, this example gets flagged by basedpyright because of a custom rule https://github.com/JelleZijlstra/unsoundness/blob/b3a94cd2793c7b5209bb9114ac5b02359df332e3/examples/cast/cast.py#L4-L5
Conversion of type `int` to type `str` may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to `object` first. (reportInvalidCast)
examples/cast/cast.py lines 4 to 5
def func(x: int) -> str:
return cast(str, x)```
this does of course work around that
def _helper(x: object) -> str:
return cast(str, x)
def func(x: int) -> str:
return _helper(x)
One idea I had was to add some mode where type checkers run in "maximum strictness" and see how many examples that catches
Somewhat dumb example but mypy has a mode that warns if you use Any anywhere, that would catch the simple Any example
Could have a summary table that shows e.g. mypy in standard mode, mypy in maximum strictness, etc. and lists whether each example succeeds in each case
I think the mutable_inplace one could be generalized without relying on the stdlib, the problem seems to be that it's unsound to add __ior__ in a subclass if the base class has __or__
you can also pass infer_variance=True nowadays too
Actually haven't been able to construct an example of this without type ignore, I think mypy is pretty good at detecting unsound use of __ior__
typeshed has a type ignore on set.__ior__
does tuple[int, str]() count? (Pyright accepts it, mypy rejects it)
this one works in both ```class X(tuple[int, str]):
pass
X()
but my repo wants to use this to create an unsafe type conversion (a function that is declared to return str but actually returns int)
I think you can leverage something like len() narrowing for that
oh yeah, I think neither of them ban overriding __len__ on fixed-length tuple subclasses
Okay, this is accepted by both mypy and pyright. They both buy that bar() returns a str, when it actually returns an int
from typing import Iterator
class Foo(tuple[int, str]):
def __iter__(self) -> Iterator[int | str]:
yield from (("foo", 42))
def bar() -> str:
a, b = Foo((1, "bar"))
return b
thanks, here's another one ```class Foo(tuple[int, str]):
pass
def func(x: int) -> str:
f = Foo(("x", x))
return f[1]
oh, do they just fall back to the tuple.__new__ signature in typeshed for tuple subclasses?!
π
But yeah, I think this is fixable and the fix is to special-case the constructor for tuple subclasses to only allow a compatible tuple
Speaking of constructors, I don't have any examples of unsound __init__ overrides yet
I think I have something but Iβll check a bit later
class SomeClass:
x: int | str
def func_a(self) -> None:
self.x = "hi"
def func_b(self) -> int:
self.x = 0
self.func_a()
x: int = self.x
return x
Iβm not sure if you are taking about something like this, or that it works, but I think it does
question regarding type variables and generics
I'd like to be able to actually access the class object associated with a TypeVar at run time but this doesn't really work since its more of a type-check only construct.
The best way around this that I've found so far is something like:
from typing import TypeVar
T = TypeVar("T")
class MyGenericClass[T]:
def __init__(self, also_my_type: type[T]) -> None:
self.attribute_of_variable_type: T = also_my_type()
where I usually set T = TypeVar("T", bound=<some kind of Protocol or Abstract class which needs to be initialized>)
and actually where this comes from is that I have a class with a generator method which yields a dictionary which works just fine at runtime but I really dislike having to use the whole dict["key"] syntax and I also dislike the lack of type hinting on the yielded dicitonary.
The dictionary's keys are determined by the caller to said class although I suppose I could have the caller pass in a TypedDict as the type variable instead but that doesn't resolve the dict["key"] syntax which I dislike.
hmmm... let me try and spin up some example code of my current solution so yall can actually suggest improvements
In many cases it's better to accept an arbitrary callable like this: ```py
from collections.abc import Callable
class MyGenericClass[T: MyProtocol]:
def init(self, also_my_type: Callable[[], T]) -> None:
self.attribute_of_variable_type: T = also_my_type()
``` it's more flexible and still lets you pass the class as is (if it supports 0 arguments in its __init__)
Do you specifically need the class object for something later? Or do you just want some way of creating a T, and you happen to have types that have the same __init__ shape?
- I don't understand how the callable approach is "more flexible" necessarily... Does this do anything more than allow me to pass in arbitrary functions which also return T (but which also take no arguments).
- I've been working on Python 3.10 for the longest time and I take it the "type hint" on the T generic acts as a... bound? constraint? Either way that's a pretty neat (and somewhat amusing) syntax.
well in this case I bound the TypeVar to an abstract class which guarantees a particular init signature π
One wishes that it did, but it doesn't, unfortunately. __init__ is specifically exempt from compabitility checks in type checkers
from abc import ABC, abstractmethod
class Foo(ABC):
@abstractmethod
def __init__(self, bar: int) -> None:
...
class Baz(Foo):
def __init__(self, bar: int, surprise: str) -> None:
...
``` ```
Success: no issues found in 1 source file
- Yes, it lets you pass in an arbitrary callable. On the flip side, it doesn't leak the detail that your interface implementors must have the same
__init__(maybe you'll have a new implementor of a protocol that must have something passed in the__init__). If you're into acronyms, this is following the "Interface Segregation Principle" -- if you only need something to be callable and produce a T, specify that as the requirement.
1b. And with the above,__init__shape can be unreliable with type checking. You could make a classmethod if you really want to accept the class object - That's the
bound=argument, yes. I got a bit confused because you used both the 3.12 syntax and the oldTypeVar()which was unused
With protocols it's even worse: type[SomeProtocol] lets you invoke a no-argument constructor because... reasons
from typing import Protocol
class Foo(Protocol):
def bar(self) -> int: ...
class TheFoo:
def __init__(self, required: int) -> None:
self.whatever = required
def bar(self) -> int:
return 69
def baz(f: type[Foo]) -> None:
print(f())
baz(TheFoo) # boom
``` the whole notion of a `type[SomeProtocol]` seems a bit iffy to me, because a protocol is a structural type describing an instance, and it doesn't reliably signal what sort of things the parent has (e.g.: the protocol might specify `@property def foo(self) -> int`, but that doesn't mean that `Implementation.foo` exists at all -- maybe the instance has it as a plain attribute and not a property)
There's a thread about this: https://discuss.python.org/t/enforcing-init-signature-when-implementing-it-as-an-abstractmethod/75690
- Okay, I definitely understand the motivation behind both the use of
Callableandclassmethodin the abstract base class.
BUT I'm now confused about something else but I'll get back to that in a minute before I get too distracted. :)
- Yes I turned myself around because I literally just started playing with the new generics syntax tonight and haven't totally made the transition in my head.
so with this solution we require that the callable take no arguments, correct?
Yep
(well, it's required that it can be called with 0 arguments, it could have e.g. an optional argument)
so coming back to #1, if a new implementor came along which does take REQUIRED arguments then it would no longer adhere to the Protocol, yes?
Updated to say "required arguments" :)
ah, okay okay
yes
e.g. with defaultdict: you can do defaultdict(int) (passing a class object), but you can also do defaultdict(lambda: defaultdict(int)) (passing a factory function)
this went completely over my head
the answer to your question is yes, it would stop working (as intended, I assume)
I meant to say that collections.defaultdict is a good example from the stdlib where it's common to pass either a class or a factory function
and so it's default_factory argument is likely Callable or something similar?
yes, it's just Callable[[], T]
question: how do I redirect from the Python docs to the associated implementation source code for CPython?
is there a good way to do this or do I just scour the CPython github?
what for in particular?
If there's a Python implementation, there's usually a link at the top of the doc page
If there isn't, it's a bit complicated. It could be in Objects/ (if it's a class like int (longobject.c for historical reasons)) or Modules/ if it's a module like math (mathmodule.c)
Standard library type definitions are located in https://github.com/python/typeshed/ in stdlib, not in the CPython repo
I'm looking for the defaultdict implementation
ah, makes sense
I assume they keep the type hints separate since they were tacked on later or...?
perhaps a backwards compatibility thing?
It imports from the C implementation here: https://github.com/python/cpython/blob/3.13/Lib/collections/__init__.py#L57
so it should be here https://github.com/python/cpython/blob/main/Modules/_collectionsmodule.c#L2198
Lib/collections/__init__.py line 57
from _collections import defaultdict```
`Modules/_collectionsmodule.c` line 2198
```c
/* defaultdict type *********************************************************/```
CPython is just one implementation of Python. There are others like PyPy and GraalVM, and they also benefit from these stubs.
Also they can evolve much faster this way, with no need to backport to old versions
aha, I also just realized that in VSCode I can also just import defaultdict and then right-click and select "go to definition" to quickly get the type hint
yep
you can also Ctrl+Click or press F12
I use Go To Definition and Go To References a lot
I would suppose that in my own code that I don't need to separate my types into a stub file since (theoretically) the implementation of Python I'm using should be able to interpret my type hints as-is anyways
and that the separate stubs are more necessary for the standard library than anything else...?
in your own (new) code, there's almost never a good reason to have stub files
it's mostly for native extensions and for libraries that don't want to adopt annotations in the codebase
and native extensions are... Python code which directly wrap some "low level" code? (guessing based off a quick google search)
yes, low level code, typically written in a compiled language
It's modules written in C, C++ or Rust most commonly
like what numpy uses for fast processing of arrays
interacting with python via the python c api
sometimes they're called C extensions, but I want to be more inclusive for those 2.5 Rust extensions in the Python ecosystem π
okay, there's probably more than 2.5
oh and C++ extensions with pybind11, tensorflow for instance
the more you know π
let me try and spin up an example of what I have now and you let me know if there's any part of it which you think doesn't make sense to have
https://gist.github.com/anthonyarusso/61b68e2867892605047629920fb1b28a took me long enough
okay so there are two files in this gist. I would actually start by looking at the second one, the simpler implementation using pure Python dictionaries and basically what I had before, and then look over the first file which is the new version using generics the way y'all suggested.
feedback and recommendations are greatly appreciated
Also is there no concise way to express a default type in the new generics syntax?
E.g.
class MyGenericClass[T: MyProtocol = MyDefaultType]:
...
That's coming in 3.14
otherwise you have to use TypeVar from typing-extensions
TypeVar was removed from the usual typing module?
No.
Then why "TypeVar from typing-extensions" and not just "TypeVar from typing"?
Because the one in typing doesn't have a default parameter if you're below 3.14
... I'm pretty sure TypeVar from typing has had the default argument even in 3.10
Unless somehow I've been implicitly importing the typing-extensions version despite clearly using from typing import TypeVar
3.13, not 3.14
oh maybe
$ python
Python 3.10.16 (main, Jan 29 2025, 04:01:22) [GCC 14.2.1 20240912 (Red Hat 14.2.1-3)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import typing
>>> T = typing.TypeVar("T", default=int)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: TypeVar.__init__() got an unexpected keyword argument 'default'
>>>
It's 3.13 and not 3.14 though as Jelle said
Hmmm maybe I'm remembering something incorrectly then. Got it
Any chance I could trouble y'all to look over this? I'm curious what your thoughts are.
Actually before that, let me make one last revision
well that's just great now its working on one computer but not the other
F = TypeVar("F", default=dict)
class BagComboSelector(Generic[F]):
# !! ERROR !!
# Incompatible default for argument "bag_combo_factory" (default has type
# "type[dict[_KT, _VT]]", argument has type "Callable[[dict[Any, Any]], F]") Mypy
# (assignment)
def __init__(self, *bags: Bag[Any], bag_combo_factory: Callable[[dict], F] = dict) -> None:
mypy isn't happy that I'm making my default factory method the dict class (which I believe should be a callabe which accepts a dict as its argument and returns a dict).
Fully type the dict. i.e. default=dict[str, list[object]]
wasn't necessary. I just updated my other laptop's VSCode profile to match the one without the error and the error went away
I don't see why typing the dict would help here. I believe all Python dictionaries conform to Callable[[dict], dict]
The best way to reproduce a typing issue is with https://mypy-play.net
this should be a pinned message tbh
I don't see that link the in existing pins
oh jk found it
mypy gives pretty bad error messages, but this is better https://mypy-play.net/?mypy=latest&python=3.12&gist=aae9a08dfd985f91ceafa8414dfa18b9
okay well my code did fail the mypy check although it supposedly "works" on both my computers now :P
that's just great
Maybe you don't have mypy running on both of your computers?
it should be? I'm using the Microsoft MyPy extension with the daemon disabled
The mypy Playground is a web service that receives a Python program with type hints, runs mypy inside a sandbox, then returns the output.
here is the failing code.
The problem with the above code is that dict (the return type of dict as a callable) isn't necessarily assignable to F. For example, you'd be allowed to do: py BagComboSelector[int]() which will still use dict as the factory. Pyright has a different approach where it understands this and won't let you call BagComboSelector[int]() without providing a factory
so if you want this to be compatible with mypy, you need an @overload
...
I understand this conceptually but I definitely don't know how I would write it out
@overload
def __init__(self: BagComboSelector[dict], *bags: Bag[Any], bag_combo_factory: Callable[[dict], dict] = ...) -> None: ...
@overload
def __init__(self: BagComboSelector[F], *bags: Bag[Any], bag_combo_factory: Callable[[dict], F]) -> None: ...
I feel like the core of the issue is that the type variable and the factory argument are decoupled in the eyes of the type checker and idk how to make them coupled
ah, I see. That's how you make overloads handle type parameters, by specifying the type variable on self
yes
and so now BagComboSelector[int]() should give an error stating it doesn't match any function signatures?
Yep
I guess it should be something like ```py
@overload
def init[K, V](self: BagComboSelector[dict[K, V]], *bags: Bag[Any], bag_combo_factory: Callable[[dict], dict[K, V]] = ...) -> None: ...
@overload
def init[F](self: BagComboSelector[F], *bags: Bag[Any], bag_combo_factory: Callable[[dict], F]) -> None: ...
... am I able to use the 3.13 Self type for this?
where?
in place of repeating BagComboSelector[dict[K, V]]
self: Self is redundant and it doesn't constrain self in any way
what about self: Self[dict[K, V]]
parameterizing Self is not allowed
E.g. if you do: py class IntComboSelector(BagComboSelector[int]): # no generic parameters class TwoParameterComboSelector[A, B](BagComboSelector[tuple[A, B]]): # new unrelated generic parameters what would Self[X] mean?
and, well, unless you need to then reference Self, you dont actually need it, just use the type itself
okay I am definitely missing something
F = TypeVar("F", default=dict)
class BagComboSelector(Generic[F]):
@overload
def __init__[K, V](
self: "BagComboSelector[dict[K, V]]",
*bags: Bag[Any],
bag_combo_factory: Callable[[dict], dict[K, V]] = dict,
) -> None:
pass
@overload
def __init__[F](
self: "BagComboSelector[F]",
*bags: Bag[Any],
bag_combo_factory: Callable[[dict], F] = dict,
) -> None:
pass
before even getting to the actual implementation this is already bleeding red
and admittedly I've lost sight of how this is meant to help
so I want to define two __init__ methods such that if someone calls BagComboSelector[<type that isn't dict>] then they will be required to use the __init__ which explicitly provides the new factory method, yes?
I should note that I'm pretty sure that I don't want the K and V typevars here. I want this class to return a "raw" totally unrestrainted python dict object from the sweep() Generator by default. (I.e. mix of key types, mix of value types).
forget this code for now. I see most of what's wrong with it after taking a step back and I fear it distracts from the actual conversation
actually I think it's not needed, it should still work if you want a BagComboSelector[dict[str, int]]
@overload
def __init__(
self: BagComboSelector[dict],
*bags: Bag[Any],
bag_combo_factory: Callable[[dict], dict] = ...,
) -> None:
pass
``` should be fine
The second overload should be py @overload def __init__[F]( self: BagComboSelector[F], *bags: Bag[Any], bag_combo_factory: Callable[[dict], F], ) -> None: pass the entire point of the overload is that a factory should be required if you want something other than dict in F
also is the ellipses operator here just saying to defer to the actual implementation signature?
In overloads and stubs, you can use ... in place of the actual default value
(since it won't be used at runtime and doesn't impact the typing)
btw, you can use from __future__ import annotations so that you don't need to quote forward referenced types
I take it this is granting me access to WIP features? Is this particular one coming in 3.14?
It's a bit complicated... but if you're not inspecting annotations at runtime, then yes
(as in, you can remove that line once you don't need to support <=3.13)
I'm also very perplexed by this def __init__[F] syntax
also it appears to be incompatible with the fact that I'm already using F at the class scope
ah, maybe you need ```py
@overload
def init[T](
self: BagComboSelector[T],
*bags: Bag[Any],
bag_combo_factory: Callable[[dict], T],
) -> None:
pass
I think this should work too though ```py
@overload
def init(
self,
*bags: Bag[Any],
bag_combo_factory: Callable[[dict], F],
) -> None:
pass
F = TypeVar("F", default=dict)
class BagComboSelector(Generic[F]):
@overload
def __init__(
self: BagComboSelector[dict],
*bags: Bag[Any],
bag_combo_factory: Callable[[dict], dict] = ...,
) -> None:
pass
@overload
def __init__(
self,
*bags: Bag[Any],
bag_combo_factory: Callable[[dict], F],
) -> None:
pass
def __init__(self, *bags: Bag[Any], bag_combo_factory: Callable[[dict], F] = dict) -> None:
# implementation
mypy says this implementation isn't compatible with the two overloads
ah yeah wait
def __init__(
self,
*bags: Bag[Any],
bag_combo_factory: Callable[[dict], F] | Callable[[dict], dict] = dict,
) -> None:
fixed :)
this is driving me crazy
do classes not conform to Callable based on their init method?
# type: ignore[the-error-mypy-shows]
this feels like a cop-out
ah, if the latter works it should be fine
it would appear that if I use this message then I will need some kind of if isinstance(bag_combo_factory, dict): ... else: conditional in my implementation to keep MyPy happy
which is frustrating me because if bag_combo_factory is dict (default value) then calling bag_combo_factory() should cast dict back to dict anyways and it clearly works at runtime
You're already working around a mypy limitation, so # type: ignore seems like a decent choice. Just make sure to understand whether/why what you're doing is sound
question: is pyright in a better state than MyPy atm?
pyright is faster (to run and to adopt new features)
also, what MyPy limitation? The fact that it isn't recognizing dict as a Callable[[dict], F]?
This whole overload thing started because mypy doesn't like ```py
class BagComboSelector(Generic[F]):
def init(self, *bags: Bag[Any], bag_combo_factory: Callable[[dict], F] = dict) -> None:
okay and you're confident that this should work in theory, correct?
I don't remember what the spec says, but it does work in pyright π
https://mypy-play.net/?mypy=latest&python=3.13&gist=d1b6bab94553326785a41fc2e425a235 I've created a simplied version
The mypy Playground is a web service that receives a Python program with type hints, runs mypy inside a sandbox, then returns the output.
the issue is Callable[[dict], T] as opposed to Callable[[dict], dict]
yes
Just like if you had py class Box[T]: def __init__(self, value: T = 69) -> None: self._value = value 69 isn't a valid T so it's not allowed as a default
whereas pyright accepts this, but notes it and makes sure the default isn't used when it doesn't make sense
generic parameters with defaults are somewhat dubious in general
would you recommend that I try to avoid this pattern or...?
if your code works it's fine, just explaining why mypy might reject some patterns pyright accepts
coming back around... what is this def method[T](...) -> ...: syntax?
https://mypy-play.net/?mypy=latest&python=3.13&gist=ee9d39c3b266e28f5a6dffcbca35a24f I think this is a good solution to my problem
The mypy Playground is a web service that receives a Python program with type hints, runs mypy inside a sandbox, then returns the output.
which is the same as fix error suggested but I think I got my head turned around until I simplified it
thank you madduck, very cool
sorry π
<enter> was too quick
def fire(
self,
*,
on_modified_cb: Callable[[FileSystemEvent, float | None], None]
| type[Undefined] = Undefined,
) -> None:
event = FileSystemEvent(str(self._path), is_synthetic=True)
if on_modified_cb is self.Undefined:
self.on_modified(event)
else:
reveal_type(on_modified_cb) β Pyright: Type of "on_modified_cb" is "((FileSystemEvent, float | None) -> None) | type[Undefined]"
on_modified_cb(event, None) β Pyright: Expected 0 positional arguments
this is code I am struggline with. Why does it say "Expected 0 positional arguments"? Neither the Callable signature in the function defition, not a constructor of Undefined would take 0 positional argumentsβ¦
If I use None instead of Undefined (a subclass of this class), it works. But I try to avoid using None to convey meaning.
It means the method itself is generic
e.g. ```py
class MyList[T]:
def zip[U](self, other: MyList[U]) -> MyList[tuple[T, U]]:
...
so would you specify a type when you call it?
no, it should be inferred
ah, the zip example makes it pretty clear. Thanks
why type[Undefined] and not just Undefined?
next question from yours truly:
how do I get @property and @overload to play nicely together?
type checkers dont't understand custom sentinels other than None and Enums
@overload
@property
def current_bag_combo(self: BagComboSelector[dict]) -> dict:
pass
@overload
@property
def current_bag_combo(self) -> F:
pass
@property
def current_bag_combo(self) -> F | dict:
"""Return either the latest combination of bag items being iterated over or the default
set of bag items if iteration using `ComboMaker.sweep()` has not yet begun."""
return self._current_bag_combo
so you need a function like ```py
def is_undefined(x: object, /) -> TypeIs[type[Undefined]]
or a guard that your type checker understands (like `isinstance` or `subclass`)
Enum. Ok. That is better. Thank you for this concise statement, which I will accept as authoritative from you π
custom sentinel⦠I am using an actual class (singleton), which is of type type[Undefined]. But see @trim tangle 's answer.
ah I see now that your default argument is the class Undefined and not an instance of it
yeah, hacky me π
... I'm not too familiar with singletons. Would an instance of a singleton class be the same as that class objects itself?
I know that there is both None and NoneType in Python although I don't fully understand the distinction. I also know that None works in the place of NoneType
a singleton is just a guaranteed-to-be-always-identical, not just equal. There can only ever be one. You can use a is b instead of a == b. This is useful. A class itself is a singleton.
I think Python / type checkers do special handling here.
that was the impression I was under
so... None is None() returns true or is it invalid to even attempt to instantiate None?
time to find out
NoneType is not callable π
None is not callable, so None() should return AttributeError
TypeError but yeah
darn
π₯Ί π
The class of None is types.NoneType, None itself is not a class. But type checkers allow you to use None to mean types.NoneType because it would be a pain to import types for such a common thing
@trim tangle pretty please π
That seems equivalent to ```py
@property
def current_bag_combo(self) -> F:
...
I mean it should be in theory but MyPy isn't keeping up... I think I've found another workaround though. Thanks
I think that's it for me for the time being btw. Thank you very much @trim tangle. Kudos and five dollars to you.
[T = Default] is supported in 3.13 right?
no π
3.14?
ahh okay, ty!
@feral wharf @gleaming goblet it's in 3.13, I was wrong before
OH?
!e
class Foo[T = int]:
pass
:warning: Your 3.13 eval job has completed with return code 0.
[No output]
I swear this was giving me an error earlier but yeah now its not π€·
I also definitely suspect there is something wrong with my MyPy configuration in VSCode... its very inconsistent about reporting errors
How do I annotate type[subclass of BaseClass]?
mypy playground
The mypy Playground is a web service that receives a Python program with type hints, runs mypy inside a sandbox, then returns the output.
If I want a type for argparse, that represents ArgumentParser, ArgumentGroup and MutuallyExclusiveGroup - i.e. all the things with add_argument
is there any approach better than writing my own Protocol?
type[BaseClass]
I could also use |, but the actual types of ArgumentGroup and MutuallyExclusiveGroup are "private"
I'd use types.SimpleNamespace with annotations
simple namespace?
Basically the same as argparse.Namespace
yeah, that's not what I'm after though
I want a type for all the things that have add_argument
So i guess writing my own Protocol is probably the cleanest solution, or I can use | if I want to be hacky?
ooh
argparse._SomethingContainer I forget the full name
am I allowed to use it though? if it's _ prefixed
Sure
_ActionsContainer, I guess
I thought that _ prefix meant that it was an implementation detail, "private", so it would be considered fragile to depend on it?
was this a response to my earlier question? If so.. type[BaseClass] is not working. I provided an example in mypy playground
dynamic class creation isn't really compatible with static type checking
Maybe you'll want NewType instead
!d typing.NewType
class typing.NewType(name, tp)```
Helper class to create low-overhead [distinct types](https://docs.python.org/3/library/typing.html#distinct).
A `NewType` is considered a distinct type by a typechecker. At runtime, however, calling a `NewType` returns its argument unchanged.
Usage...
!e ```py
from typing import TypeVar, NewType
class MyMetaclass(type):
"""Simple metaclass that creates classes."""
def __init__(
cls,
name: str,
bases: tuple[type],
attrs: dict[str, str]
) :
print("__init__")
class BaseClass(metaclass=MyMetaclass):
"""Base class."""
Runtime: Works perfectly
MyClass = NewType("MyClass", BaseClass)
instance = MyClass(BaseClass())
print(f"Created: {instance}")
print(f"Is subclass? {isinstance(instance, BaseClass)}")
:white_check_mark: Your 3.13 eval job has completed with return code 0.
001 | __init__
002 | Created: <__main__.BaseClass object at 0x7efcb9cf96a0>
003 | Is subclass? True
It does need an actual instance of BaseClass though
it's usually used for primitive types like int or str, so I'm not sure if it's best for your usecase
def create_starlette_application(*args, **kwargs) -> Starlette:
return Starlette(*args, **kwargs, routes=_routes)
how to hunt this
How can I add type hinting in a Lambda function?
And also how can I put a custom type hints?
You can hint a lambda function if you assign it to a variable but that kinda defeats the point of a lambda
Type checkers might be able to infer what a lamba does though
!e
from typing import reveal_type
reveal_type(map(lambda x: x * 1.5, [1,2,3]))```
:white_check_mark: Your 3.13 eval job has completed with return code 0.
Runtime type is 'map'
(At static type checking time this say map[float])
from typing import reveal_type
import ffmpeg # https://pypi.org/project/typed-ffmpeg/
out = ffmpeg.probe_obj("")
reveal_type(out)
assert out is not None
reveal_type(out)
reveal_type(out.streams)
assert out.streams is not None
reveal_type(out.streams)
reveal_type(out.streams.stream)
assert out.streams.stream is not None
reveal_type(out.streams.stream)
s = sorted(out.streams.stream, lambda s: s.index)
reveal_type(s)
β― uv run --with mypy mypy --strict t.py
t.py:6: note: Revealed type is "Union[ffmpeg.ffprobe.schema.ffprobeType, None]"
t.py:8: note: Revealed type is "ffmpeg.ffprobe.schema.ffprobeType"
t.py:9: note: Revealed type is "Union[ffmpeg.ffprobe.schema.streamsType, None]"
t.py:11: note: Revealed type is "ffmpeg.ffprobe.schema.streamsType"
t.py:12: note: Revealed type is "Union[builtins.tuple[ffmpeg.ffprobe.schema.streamType, ...], None]"
t.py:14: note: Revealed type is "builtins.tuple[ffmpeg.ffprobe.schema.streamType, ...]"
t.py:15: error: No overload variant of "sorted" matches argument types "tuple[streamType, ...]", "Callable[[Any], Any]" [call-overload]
t.py:15: note: Possible overload variants:
t.py:15: note: def [SupportsRichComparisonT: SupportsDunderLT[Any] | SupportsDunderGT[Any]] sorted(Iterable[SupportsRichComparisonT], /, *, key: None = ..., reverse: bool = ...) -> list[SupportsRichComparisonT]
t.py:15: note: def [_T] sorted(Iterable[_T], /, *, key: Callable[[_T], SupportsDunderLT[Any] | SupportsDunderGT[Any]], reverse: bool = ...) -> list[_T]
t.py:16: note: Revealed type is "Any"
Found 1 error in 1 file (checked 1 source file)
how come s is Any?
https://github.com/livingbio/typed-ffmpeg/blob/main/src/ffmpeg/ffprobe/schema.py#L295
its because of the | None here, int | None is not comparable so you end up with an invalid call so the result is any
src/ffmpeg/ffprobe/schema.py line 295
index: int | None = None```
thank you
such a stupid oversight from me lol
bro wht
@dataclass
class SubparserInfo:
field_name: str
nested_info: dict[str, tuple[type, 'SubparserInfo' | None]]
How would I make python happy with this?
put the entire type in the forwardreference quotes
because currently you're doing a bitwise or between a string and none, and since annotations are evaluated as normal expressions - thats an error
alternatively you could from __future__ import annotations and just not need quotes anymore
yeah I figured as much
I didn't realize the quotes could just go around the whole thing though
thanks!
somehow I've never run into this case before
is SQLModel a good library for typesafe sqlite in python?
I noticed in SqlAlchemy and PyCord libraries that there's runtime functionality being exposed through the type annotations, I get that this is because annotations are evaluated as normal expressions, but I'm confused about where this paradigm arose. I am using 3.11 and pylance hates this
What is the type map? Why not a sequence or iterable?
map and filter are classes
so is reversed, though confusingly calling reversed doesn't always produce an instance of it
π§
type comments?
are dataclasses just classes of type hints?
no, they also add runtime behavior
There is typing.dataclass_transform in 3.11+ to get that typing behaviour without runtime effect
https://docs.python.org/3/library/typing.html#typing.dataclass_transform
!d typing.Protocol is what you're thinking of
class typing.Protocol(Generic)```
Base class for protocol classes.
Protocol classes are defined like this...
this might be a solution to the question I'm about to ask... very interesting. thank you
so here's my conundrum
I want my calling code to look and behave as follows:
if __name__ == "__main__":
my_combo = Combo(
Selection("a", "b", "c", default="X"),
Selection(1, 2, 3, default=0),
Selection(0.125, 2.77, 3.14, default=math.nan),
)
for c in my_combo.combos():
print(c)
hmm how to explain
class Combo[A, B, C]:
def __init__(self, a: Selection[A], b: Selection[B], Selection[C]) -> None:
...
I want the caller to see an interface which looks like this ^
but I want the the code (and type checker) to also register a new dataclass (or similar) which basically looks and feels like:
@dataclass
class _ComboData:
a: A
b: B
c: C
and have items of this type be yielded from the combos() method
I suppose I could just have a nested class definition...
but what I'd like even more is some way to get to this pattern without as much boilerplate
maybe a class decorator like
@dataclass_from_args_typevars
class MultipleContainers:
def __init__(self, a: Container[A], b: Container[B], c: Container[C]) -> None:
...
which defines both the outer class and the dataclass with attributes of type A, B, and C.
not directly related, but any idea how I can condense isinstance(a, MyClass) and isinstance(b, MyClass) and ... into a single areinstance(a, b, ..., class_or_tuple = MyClass)?
I take it this just isn't supported yet?
...
Is it possible to make, say, a class decorator, which has access to the types over which the class which it is decorating is generic over? (during static type analysis, of course)
def dataclass_from_contained_typed[T: type](cls: T[A]) -> T:
cls.new_attr: A = ...
have I gone too far?
Is this just type system brainrot?
or is what I'm looking for less of a class decorator and more of a metaclass... ?
No, you can't subscript a TypeVar. I'm not entirely sure what you're trying to do but it might be possible
No, you can't condense that. And I'm not sure adding a more concise version of that code would help readability
Well yeah I didn't mean I actually wanted the subscript. It's the behavior I had described that I'm actually looking for
Accessing an input variable's type variables
Not in general, sometimes you can use e.g. types.get_original_bases
I'm thinking of applying this to a variadic, so the number of variables to apply the type guard to is arbitrary
... Bases meaning base classes? That's not quite what I'm looking for here.
I definitely feel like I'm trying to overextend pythons type system though if I'm being honest
Is it possible to type a dict that's keys are classes and values are instances of the key class
that doesn't preserve the constraint that each value is an instance of its key
you can use a protocol that has a TypeVar in getitem
from typing import overload, TypeVar, Literal, reveal_type
_T = TypeVar('_T')
@overload
def option(*, required=Literal[True], choices: list[_T] | None = ...) -> _T:
...
@overload
def option(*, default: _T, required=Literal[False], choices: list[_T] | None = ...) -> _T:
...
def option(*, default = None, required = False, choices = None):
return default
x = option()
reveal_type(x)
I'm somewhat surprised here that x's type is Any
It seems to me like it should pick the narrowest type that works, which is None
is there any way to make this work other than adding an overload?
I'm writing a function that provides an API to argparse, to add an optional argument, and I'm currently at 7 overloads π
# Typing overloads:
# 1. required=True, no default, no const, nargs=None
# 2. required=True, no default, no const, nargs=multiplicity
# 3. required=False, no default, yes const, nargs=None or ?
# 4. required=False, no default, no const, nargs=None or multiplicity
# 5. required=False, default provided, yes const, nargs= None or ?
# 6. required=False, default provided, no const, nargs=None
# 7. required=False, default provided, no const, nargs=multiplicity
Actually I think I can merge the first two. Still, it's quite a bit.
The middle overloads seemingly have to be provided, because of the reduced example above
Probably not your problem but required=Literal[True] is wrong, Literal[True] should be the type not the default
oops thanks
when i fix that mypy gives another confusing error, IMHO
scratch2.py:17: error: All overload variants of "option" require at least one argument [call-overload]
As for the behavior of option(), Any makes sense to me. It matches the first overload only, and because the choices argument isn't given there's no way to constrain the value of the TypeVar
You probably want required: Literal[False] = False (or ...) for the second overload
i.e., it should have a default for the overload that matches the runtime default
do they have to have the same default, or just any default?
though in that case I think the option() case should still throw an error, because the second overload still requires the default argument
i.e. = ...
either works, the actual value of the default is ignored
gotcha. okay, so I've fixed that - why is the first overload matched? required defaults toFalse
so the second overload is matched
In the code as you showed it, the first overload had defaults for both arguments
While the second had one required argument
So a call with no arguments can only possibly match the first overload
from typing import overload, TypeVar, Literal, reveal_type
_T = TypeVar('_T')
@overload
def option(*, required: Literal[True] = ..., choices: list[_T] | None = ...) -> _T:
...
@overload
def option(*, default: _T = ..., required: Literal[False] = ..., choices: list[_T] | None = ...) -> _T:
...
def option(*, default = None, required = False, choices = None):
return default
x = option()
reveal_type(x)
sorry, this is what it looks like now
scratch2.py:17: error: Need type annotation for "x" [var-annotated]
scratch2.py:18: note: Revealed type is "Any"
So I think you want to remove the = ... for the required param on the first overload. That overload should only be matched if the user actually writes required=True, not if they don't pass the argument
I guess I wouldn't expect that to make a difference since the default is False, but alright
I did remove it, and it does not seem to make a difference
The overload spec is here https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation. The later parts are quite convoluted but the first two steps usually matter: first check whether the signature matches at all (based on required/not-required arguments) and then check whether the types match
The default on the implementation doesn't matter for overload evaluation
Yeah now it hits the second overload and it still has no way to decide how to constrain the value of T
why can't it choose _T is None?
Why would it?
I guess this comes back to not using the default of th eimplementation
so, if I give overload 2, default: _T = None
I get this error
scratch2.py:10: error: Incompatible default for argument "default" (default has type "None", argument has type "_T") [assignment]
scratch2.py:10: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True
scratch2.py:10: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase
scratch2.py:17: error: Need type annotation for "x" [var-annotated]
scratch2.py:18: note: Revealed type is "Any"
Found 2 errors in 1 file (checked 1 source file)
This error seems wrong to me
pyright does infer x as None in that case
yeah that mypy error is a bit confusing here
it seems overzealous insofar as it assumes my intention here was implicit optional, which it wasn't
there's nothing wrong with this, it just constrains _T to be a type that allows None as a value
_T could be None or it could be int | None, etc
That's indeed how pyright works here
Gotcha. I mean I can see why the behavior here was useful but in hindsight, I think it would have been better if it was restricted to situations where the type was concrete rather than generic
I've seen mention that defaults on parameters that use TypeVars can lead to some sketchy behavior, I haven't looked deeply into it though
But I believe that's why mypy is stricter than pyright here
I suppose there is no way to opt out of this?
Add more overloads I guess
# 1. required=True, no default, no const, nargs=anything but ?
# 2. required=False, no default, yes const, nargs=None or ?
# 3. required=False, no default, no const, nargs=None or multiplicity
# 4. required=False, default provided, yes const, nargs= None or ?
# 5. required=False, default provided, no const, nargs=None
# 6. required=False, default provided, no const, nargs=multiplicity
this is where I'm at right now π’
def option(
*name_or_flags,
group: WhichGroup = None,
use_field_name=True,
# default is something that occurs both in dataclasses and argparse, the handling is a bit nuanced
default=None,
# argparse arguments
nargs: NargsType = None,
const: Any = None,
# skip default as we already handle that via dataclass
type=None,
choices=None,
required=False,
help=None,
metavar=None,
deprecated=False,
# Other dataclass arguments
init: bool = True,
repr: bool = True,
hash=None,
compare=True,
metadata=None,
kw_only: bool = False,
):
and the function has quite a few arguments
are you reimplementing argparse? π
@dataclass
class MyArgs:
first_arg: int = positional()
glug: str | None = option()
second: int | None = option()
ex1: ClassVar[ExclusiveGroup] = ExclusiveGroup()
foo: int | None = option(group=ex1, help="this is forwarded back to argparse")
bar: int | None = option(group=ex1, metavar="so_is_this")
sub: SubCommand1 | SubCommand2 | None = subparsers()
I'm aiming for a UI like this
so, I'm really trying to thread a sort of needle here; I want option to infer the most sensible defaults, but also give good type errors as much as possible
for example, you obviously want glug: str = option() to be a type error, that's one of the most common mistakes to make
Is this what you want?
from typing import overload, TypeVar, Literal, reveal_type
_T = TypeVar('_T')
@overload
def option(*, required: Literal[True], choices: list[_T] | None = ...) -> _T:
...
@overload
def option(*, required: Literal[False] = ..., choices: None = ...) -> None:
...
@overload
def option(*, required: Literal[False] = ..., choices: list[_T]) -> _T | None:
...
@overload
def option(*, default: _T = ..., required: Literal[False] = ..., choices: list[_T] | None = ...) -> _T:
...
def option(*, default: object = None, required: bool = False, choices: object = None) -> object:
return default
reveal_type(option())
reveal_type(option(choices=[1]))
reveal_type(option(default=42))
reveal_type(option(choices=[1], default=42))
gets me main.py:25: note: Revealed type is "None" main.py:26: note: Revealed type is "Union[builtins.int, None]" main.py:27: note: Revealed type is "builtins.int" main.py:28: note: Revealed type is "builtins.int"
probably
I did have this working, and I left a note for myself that I needed this overload to make it work
but then I needed to add even more overloads to support nargs and lists, so I was hoping to cut some down
though I'm surprised you needed 4 overloads, I thought it would only require 3
there might be a smaller solution
from typing import overload, TypeVar, Literal, reveal_type
_T = TypeVar('_T')
@overload
def option(*, required: Literal[True], choices: list[_T] | None = ...) -> _T:
...
@overload
def option(*, required: Literal[False] = ..., choices: list[_T] | None = ...) -> _T | None:
...
@overload
def option(*, default: _T, required: Literal[False] = ..., choices: list[_T] | None = ...) -> _T:
...
def option(*, default = None, required = False, choices = None):
return default
x = option()
reveal_type(x)
Yeah was just trying that too, it does work but it doesn't force the right solution for _T so mypy could infer something else
could? Based on what?
I do see what you mean that it doesn't force _T at all, but I guess if mypy behaves this way then it should always behave this way
Solving TypeVars is necessarily based on heuristics to some extent
I'd assume that if _T is totally unconstrained then it would default to object
that's the least constrained type, and object | None = None
Mypy often seems to default to Never, which I think is why your example with fewer overloads works correctly
hmm, wouldn't object as the default work too?
the option() call matches def option(*, required: Literal[False] = ..., choices: list[_T] | None = ...) -> _T | None:
_T is Never, so we get Never | None = None
Anything would work arguably, I guess the question is what is most useful
Yeah, I just meant that it would result in the type deduction we see above
Since object | None is also None
Actually I guess it's not
So much for that
Thanks for your help Jelle
@oblique urchin if u had to learn python from scrath rn how would u do it [sry for the ping ]
i'll answer for myself here as someone with a background in teaching python
just do what you're interested in; explore, google things, try to figure your way out to the outcome you want, don't let lack of certainty hold you back
it's all about solving problems than anything else really. try to be clever, experiment, and just do things
i don't think i know of a better way
also, please use the right channel next time :)) this one is for the type hinting space specifically
Mb and tysm 
tptools/tpsrv/cli.py:77: error: Missing type parameters for generic type "Task" [type-arg] β this is about asyncio.Task. I cannot see any types on https://github.com/python/cpython/blob/main/Lib/asyncio/tasks.py#L56, so I wonder: what type is Task generically wrapping?
Lib/asyncio/tasks.py line 56
class Task(futures._PyFuture): # Inherit Python Task implementation```
Task is generic in typeshed.
stdlib/_asyncio.pyi line 52
class Task(Future[_T_co]): # type: ignore[type-var] # pyright: ignore[reportInvalidTypeArguments]```
_T = TypeVar("_T")
_T_co = TypeVar("_T_co", covariant=True)
fine. But what does_T_co even mean?
_co is just a mnemonic for covariant
it's the return type of the coroutine of the task?
when you await the task (or call its result method) you should get a value of type _T_co
See here for explanation of covariance. https://typing.python.org/en/latest/reference/generics.html#variance-of-generic-types
man, this is science
typing is a science
I kinda get it I think. Thank you for the link. This will take a few days to grok.
someone should make an easily digestible tutorial on generics
With a table comparing the features of each
of each what?
variance
ah
I just do what the type checker tells me to do
mypy: typevar should be covariant
me: ok
hello guys! can you please recommend me some good resources to understand or master type-hinting.
Thank you! π
Official docs: https://typing.python.org/en/latest/
I have a work in progress tutorial: https://decorator-factory.github.io/typing-tips/tutorials/main/0-start-here/ but it only covers the very basics
thank you so much. i appreciate it. your tutorial looks amazing.
Hey. Maybe you guys have an idea? I tried to improve typing on a function argument from Iterable to ItemsView (as the function actually accepts some dict.items() and runs len on it), and started getting an error I don't understand.
from typing import Iterable, ItemsView, TypeVar, Any
from collections.abc import Callable
T = TypeVar('T')
def my_map_good(func: Callable[..., T], iterable: Iterable[Any], *args: object) -> list[T]:
def _wrapped_func(func_and_args: tuple[Callable[..., T], *tuple[Any, ...]]) -> T:
func = func_and_args[0]
args = func_and_args[1:]
return func(*args)
func_and_args = [(func, it_arg, *args) for it_arg in iterable]
result: list[T] = list(map(_wrapped_func, func_and_args))
return result
def my_map_bad(func: Callable[..., T], iterable: ItemsView, *args: object) -> list[T]:
def _wrapped_func(func_and_args: tuple[Callable[..., T], *tuple[Any, ...]]) -> T:
func = func_and_args[0]
args = func_and_args[1:]
return func(*args)
func_and_args = [(func, it_arg, *args) for it_arg in iterable] # <=======
# error: Argument 1 to "map" has incompatible type "Callable[[tuple[Callable[..., T], *tuple[Any, ...]]], T]";
# expected "Callable[[tuple[object, ...]], T]" [arg-type]
result: list[T] = list(map(_wrapped_func, func_and_args))
return result
if you do ItemsView[Any, Any] it goes away (on pyright, atleast)
not sure what the function is doing, though, seems pretty weird
i'd imagine something like
def map_partial[First, **Rest, Result](fn: Callable[Concatenate[First, Rest], Result], iterable: Iterable[First], *args: Rest.args, **kwargs: Rest.kwargs) -> list[Result]:
return [fn(arg, *args, **kwargs) for arg in iterable]
makes more sense
though tbh just inlining this avoids the complex typing thing
Thanks! On mypy ItemsView[Any, Any] doesn't change anything: https://mypy-play.net/?mypy=latest&python=3.12&gist=0e0234e5753bd351ad8d6b519261dc99
I realize this code snippet is weird - the real code does more than delegate the execution. This was just stripped to the bare minimum demonstrating the typing mystery.
The mypy Playground is a web service that receives a Python program with type hints, runs mypy inside a sandbox, then returns the output.
(You'd need to 'Run' after clicking the link)
Hey! Quick question:
In modern Python (3.10+), is it better to use from __future__ import annotations instead of if TYPE_CHECKING: to avoid circular imports and type hints?
Which one is considered more idiomatic or "clean" today?
these two things don't solve the same problem, so you can't use one instead of the other.
if TYPE_CHECKING means
if Falseat runtimeif Trueat type checking time (which does not exist, it's just a hint to your type checker to equally treat a set of statements that never execute because it contains extra info for typing)
(roughly)
therefore if you want to use module-level "circular-origin" annotations you need to (1) place all circular typing-only imports under if TYPE_CHECKING and (2) ensure these annotations aren't evaluated at runtime, because you will get a NameError (which is why i avoid if TYPE_CHECKING in modules with pydantic models)
up until Python 3.14 (PEP 749 target version), from __future__ import annotations is helpful with (1), so it complements (not replaces) if TYPE_CHECKING.
it is helpful because it turns all annotations in a module into strings, so they are effectively never truly evaluated as expressions (unless you use Pydantic, for example, or run typing.get_type_hints() etc.)
if you want to leverage if TYPE_CHECKING without PEP 563 (__future__.annotations) you either need Python 3.14+ that implements PEP 749 or quotes around circular annotations (foo: "Foo")
@loud breach does that help?
I wasnβt very clear earlier, but yes, Iβm specifically talking about circular imports, and your replies are really helping me understand things better.
Currently, I have classes like:
# manager.py
from .core import Workflow
from .history import HistoryLogger
from .serializer import WorkflowSerializer
class WorkflowManager:
def __init__(
self,
max_active: int = 2,
serializer: Optional[WorkflowSerializer] = None,
history: Optional[HistoryLogger] = None,
):
...
# core.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .manager import WorkflowManager
@dataclass
class Workflow:
manager: "WorkflowManager"
...
# serializer.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .manager import WorkflowManager
class WorkflowSerializer:
def save(self, manager: "WorkflowManager") -> None:
...
# history.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .core import Workflow
class HistoryLogger:
async def append(self, workflow: "Workflow") -> None:
...
With the TYPE_CHECKING, I donβt get any ImportError: cannot import name .. during test runs, and mypy doesnβt complain either, everything works.
From what I understand, using from __future__ import annotations would let me drop the quotes around class names, but it wonβt solve the circular import itself if the modules depend on each other at runtime.
So if I got it right, combining both techniques (future annotations + TYPE_CHECKING) makes the code cleaner and avoids circular import issues entirely.
Is that correct?
Thank you so much for your response, it was really clear! π
From what I understand, using from
__future__ import annotationswould let me drop the quotes around class names, but it wonβt solve the circular import itself if the modules depend on each other at runtime.
exactly
So if I got it right, combining both techniques (future annotations + TYPE_CHECKING) makes the code cleaner and avoids circular import issues entirely.
yes, but not entirely because if you evaluate those strings through the typing API, you're gonna get name errors because these things are never imported
i worked on something that could roughly solve this by delaying imports until first reference, maybe i'll go back to that hacky project
it could be nice if it's scoped only for annotations, or maybe it was not delaying but special types in place of type imports that would be treated properly
ideally python would have type imports like TypeScript at some point
@oblique urchin do you know of any past efforts in that area?
although idk, i think PEP 749 maybe solves some of this, will read deeper into it
People have talked about it, and there's been a general lazy imports PEP that got rejected
But no effort that's gotten far to get type imports specifically
yes, pep 690
What do you mean when you write "the typing API" ? Are you talking about IDE autocompletion?
typing.get_type_hints, typing._eval_type and typing.ForwardRef._evaluate
the typing API is used at runtime
commonly by libraries that give semantics to type annotations, such as Pydantic
(that one doesn't use typing.get_type_hints but it does wrap typing._eval_type afair)
can you still manually specify variance with the [] typevar syntax
no
i guess something like
class PageStrategy[R](ABC, Generic[Vd]):
doesn't work either
why do you want this?
Isn't variance inferred?
Vd is bound to PageVendor = PageSimpleVendor | PageMultiVendor, but it's invariant because it's in both the method arguments and return params
i'm working with frozen types so i really don't care that much
so i'd like them to be covariant
invariance means i can't do
SyncPageStrategy(CSLTrack) if client.is_sync() else AsyncPageStrategy(CSLTrack)
because it gets bound to PageMultiVendor or PageSimpleVendor specifically and then one of them conflicts (sync is simple and async is multi)
i'll probably just do
R = TypeVar('R')
Vd = TypeVar('Vd', bound=PageVendor, covariant=True)
class PageStrategy(ABC, Generic[R, Vd]):
like old times
Is there any way for me to tell the type checker that it's not possible for y to be unbound or do I just type: ignore?
Add y = height - 1 before the for loops
You can ignore a specific error: ```py
def f() -> None:
for x in range(3):
print(x)
print(x, ".") # pyright: ignore[reportPossiblyUnboundVariable]
Another question, sorry
Is this the best way to do this or is there a standard way to tell it that it can't return None?
I think I actually do like this way but just wondering if there's a well established method
assert False, "unreachable" is what I'd write
but what you do is fine too
a flake8/ruff rule may suggest replacing it with raise AssertionError("unreachable")
that rule is silly in my opinion. but your code, your rules
It's because -O
to be honest does anybody actually run python with -O?
Probably not, but it can improve speeds slightly, so... maybe. I think the ruff rule does make sense though, ruff is generally pretty strict, and raise AssertionError("unreachable") isn't that much harder to type compared to assert False, "unreachable" and since it is also more reliable, why not prefer it
it might even be more readable, even if someone isn't as familiar with asserts, they likely saw raise before
folks saying python is slow without using -O smh
not silly question to a silly example:
from typing import Literal, TypeVar
T = TypeVar("T", bound=Literal[True])
def should_be_assignable() -> Literal[True]:
return True
def parametrized(x: T) -> T:
return should_be_assignable()
since T is bounded to Literal[True], which is a fully static type that represents a single runtime value
and therefore, since there is no consistent subtype of Literal[True] (as the upper bound of T) that isn't assignable to other consistent subtype of Literal[True] (as the upper bound of T), because Literal[True] represents a set with only one value
shouldn't any Literal[True] value be always assignable to T, where it's bound (to Literal[True], obviously)?
technically yes, but I'm not at all surprised that most (if not all) type checkers can't figure that out, they just don't make the assumption of "this type can't have any subtype", even when if that is the case with literals.
also, that code is so cursed, lol
If the type checker were smarter, it wouldn't let you have a typevar bound for a single literal.
been doing https://github.com/python/typeshed/pull/14439, my whole day is cursed
mypy does have special cases for final bounds in some cases iirc
nvm i was confusing typevar constraints with bounds in my memories lol
yeah, it doesn't make sense to use a typevar if its contraints/bound don't span at least two different runtime values (or types, technically)
that's also maybe why you need at least two constraints... and bound maybe was assumed to always have further materializations
let's see what if i do TypeVar("T", bound=tuple[None, None])
yeah, same
it's also noted in the spec, section on literals, that it doesn't make sense to use literals as typevar bounds
