#type-hinting
1 messages ยท Page 31 of 1
why are these contravariant?
class SupportsDunderLT(Protocol[_T_contra]):
def __lt__(self, other: _T_contra, /) -> bool: ...
class SupportsDunderGT(Protocol[_T_contra]):
def __gt__(self, other: _T_contra, /) -> bool: ...
class SupportsDunderLE(Protocol[_T_contra]):
def __le__(self, other: _T_contra, /) -> bool: ...
SupportsRichComparison uses a union while SupportsAllComparisons is basically an intersection between the individual protocols
yes but that should still be fine, it's enough that either of those types in the union is used as the type there
.
I guess the type checker isn't capable of expressing this "fallback" behaviour in case one of the operator definitions are missing
so, a pyright bug?
I had a similar problem with iterables that only implement __len__ and __getitem__ but not __iter__, they fail to satisfy the Iterable protocol even though they are iterable at runtime
so, would it be safe for me use the supportsallcomparisons? what if my type only declared __gt__ and __lt__?
such types should still work with that func, but probably wouldn't meet supportsallcomparisons
it would give false positive (fake errors) so it's safe
the code inside heapify itself is sound
well okay yeah, it would be safe, but it wouldn't be a nice experience for users, they'd have to type ignore or make type wrappers
didn't expect this to be so annoying
maybe you can wrap your function
from collections.abc import Callable, MutableSequence
from typing import TYPE_CHECKING, TypeVar
if TYPE_CHECKING:
from _typeshed import SupportsAllComparisons, SupportsRichComparison
_T = TypeVar("_T", bound=SupportsAllComparisons)
def _heapify(lst: MutableSequence[_T]) -> None:
"""Convert a list into a **max** heap."""
parent: Callable[[int], int] = lambda index: (index - 1) // 2 # noqa: E731
# Iterate through the list in reverse order, skipping the first (index 0)
# (note: we can't use enumerate here, as it wouldn't work with reversed)
for idx in range(len(lst) - 1, 0, -1):
elem = lst[idx]
parent_idx = parent(idx)
parent_elem = lst[parent_idx]
if elem > parent_elem:
lst[idx], lst[parent_idx] = parent_elem, elem
T = TypeVar("T", bound=SupportsRichComparison)
def heapify(lst: MutableSequence[T]) -> None:
return _heapify(lst) # type: ignore
def main() -> None:
x = [5, 8, 1, 3, 6, 10, 55]
heapify(x)
assert x[0] == 55
heapq.heapify just does list[Any] lol
that way the # type: ignore is in your own code (instead of the users' code) while you still perform proper type checking inside the main logic. you can also add a comment explaining why this is the case
whats going on here
well yeah, that would work
discussing how to work around type checkers failing to consider fallback behavior of some dunder methods
specifically when it comes to comparisons, like __gt__, __lt__, ...
this was my initial q: #type-hinting message
I think this makes the most sense, it's quite annoying, but oh well
:incoming_envelope: :ok_hand: applied timeout to @peak karma until <t:1716393878:f> (10 minutes) (reason: burst spam - sent 8 messages).
The <@&831776746206265384> have been alerted for review.
How do I annotate a context manager that always suppresses exceptions? This doesn't seem to work
from __future__ import annotations
from types import TracebackType
from typing import Literal, Protocol, overload
from typing_extensions import Self, assert_never
class ContextManagerAlwaysReraise(Protocol):
def __enter__(self) -> Self: ...
@overload
def __exit__(self, __type: type[BaseException], __value: BaseException, __tb: TracebackType) -> Literal[False]: ...
@overload
def __exit__(self, __type: None, __value: None, __tb: None) -> None: ...
@overload
def __exit__(self, __type: type[BaseException] | None, __value: BaseException | None, __tb: TracebackType | None) -> Literal[False] | None: ...
class ContextManagerAlwaysSuppress(Protocol):
def __enter__(self) -> Self: ...
@overload
def __exit__(self, __type: type[BaseException], __value: BaseException, __tb: TracebackType) -> Literal[True]: ...
@overload
def __exit__(self, __type: None, __value: None, __tb: None) -> None: ...
@overload
def __exit__(self, __type: type[BaseException] | None, __value: BaseException | None, __tb: TracebackType | None) -> Literal[True] | None: ...
class ContextManager(Protocol):
def __enter__(self) -> Self: ...
@overload
def __exit__(self, __type: type[BaseException], __value: BaseException, __tb: TracebackType) -> bool: ...
@overload
def __exit__(self, __type: None, __value: None, __tb: None) -> None: ...
@overload
def __exit__(self, __type: type[BaseException] | None, __value: BaseException | None, __tb: TracebackType | None) -> bool | None: ...
def foo(ctx: ContextManagerAlwaysReraise):
with ctx:
raise ValueError
assert_never(0)
def bar(ctx: ContextManagerAlwaysSuppress):
with ctx:
raise ValueError
assert_never(0) # This line should fail
def baz(ctx: ContextManager):
with ctx:
raise ValueError
assert_never(0)
(I'm using Pyright btw)
assert_never(0) should never fail, 0 is not of type Never
Actually that is why it should fail, since int cannot be assigned to an argument of type Never.
But that's besides the point, the idea is that I want that line to be interpreted as "reachable" by Pyright
currently it's interpreted as being unreachable
Seems that this works
class ContextManagerAlwaysSuppress(Protocol):
def __enter__(self) -> Self: ...
def __exit__(self, __type: type[BaseException] | None, __value: BaseException | None, __tb: TracebackType | None) -> Literal[True]: ...
Not sure why allowing __exit__ to return None when None is passed to each argument would break it though. From my understanding, None is passed to each argument only when no exception is being thrown inside the context manager, so it should have nothing to do with whether the context manager suppresses exceptions.
so something like this should still work in theory
class ContextManagerAlwaysSuppress(Protocol):
def __enter__(self) -> Self: ...
@overload
def __exit__(self, __type: type[BaseException], __value: BaseException, __tb: TracebackType) -> Literal[True]: ...
@overload
def __exit__(self, __type: None, __value: None, __tb: None) -> None: ...
i dont think you can, and dont think you should
i mean, it probably will work with some typechecker that supports this, but might not work with other
contextlib.suppress could also be potentially defined as this:
# Need to update _ExitT_co to support TypeGuard
_T_co = TypeVar("_T_co", covariant=True)
_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None | TypeGuard[object], default=bool | None)
@runtime_checkable
class AbstractContextManager(Protocol[_T_co, _ExitT_co]):
def __enter__(self) -> _T_co: ...
@abstractmethod
def __exit__(
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
) -> _ExitT_co: ...
E = TypeVar("E", bound=BaseException)
class suppress(AbstractContextManager[None, TypeGuard[type[E]]], Generic[E]):
def __init__(self, *exceptions: type[E]) -> None: ...
def __exit__(
self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None
) -> TypeGuard[type[E]]: ...
(Whether it's worth the effort for type checkers to support this is another question altogether xD)
If you're starting in a vacuum, I'd pick pyright
I do have experience with type-hinting for a few years, I'm just tired of using PyCharm's builtin linter
pyright does have an extension for pycharm, apparently https://github.com/InSyncWithFoo/pyright-for-pycharm
I don't use pycharm too much, but from anecdotes and questions in forums the type checker is kind of a letdown, especially for such a powerful editor
thats neat, thank you
yeah thats exactly why I want to change ...
while I'm at it, does PyRight have any bugs when type checking?
cuz PyCharm has many false warnings
Definitely put a type checker somewhere in your CI pipeline so that you don't forget to read its complaints
Guys can you explain to me what annotations are? I read docs but can't get it
Which docs did you read?
Python official docs
Which page specifically?
https://mypy.readthedocs.io/en/stable/ are another good place to start
I read PEP 484
Oh thanks : )
is thre any way to make an enum of functions?
I want to inform the type system that I have a fixed number of functions of the same type
you can, but I question whether or not you should. it should be sufficient to just provide a protocol that matches that same type
from typing import Protocol
class Something(Protocol):
def __call__(self, arg1: int, arg2: int) -> int:
...
def foo(arg1: int, arg2: int) -> int:
...
def bar(arg1: int, arg2: int) -> int:
...
def function_taking_foo_or_bar(fn: Something):
...
presumably if these functions are all being used the same way, this is sufficient and more flexible, but
from enum import Enum
def _foo(arg1: int, arg2: int) -> int:
...
def _bar(arg1: int, arg2: int) -> int:
...
class FnEnum(Enum):
FOO = _foo
BAR = _bar
def function_taking_foo_or_bar(fn: FnEnum):
...
works fine
is there a way to get a "fully typed" set of overloads in both interface and implementation without duplicating signatures?
@overload
def process(response: None) -> None:
...
@overload
def process(response: int) -> tuple[int, str]:
...
@overload
def process(response: bytes) -> str:
...
def process(response):
if response is None::
return process_impl_none()
if isinstance(response, int):
return process_impl_int(response)
return process_impl_bytes(response)
def process_impl_int(response: int) -> tuple[int, str]:
# implement
def process_impl_bytes(response: bytes) -> str:
# implement
I don't think so currently (if someone has a neat trick, I'd love to see it). If intersections and the thing to do TypeOf[fn] were to exist, you probably could since these dont have overlapping domains.
couldn't the overload decorator have helped us with this
@overload("_none")
def process(response: None) -> None:
# actually implement this now
def process(response):
if response is None:
return process_none()
# etc
are you looking for functools.singledispatch?
no
that's a) limited to single dispatch, and b) it's going to start processing type signatures at runtime
I actually got interested in this specifically because I was helping someone who started with singledispatch
they had a member function of Foo that returned a Foo
which seemingly broke singledispatch, probably because singledispatch was trying to process that Foo return type annotation eagerly before it made sense
Is there any reason though, even now, that an argument couldn't be added to the overload decorator, to solve this issue?
It even seems backwards compatible
It could be, but do you envision this doing something like sys._getframe(1).f_globals[func.__name__ = argument] = func or something? that seems pretty confusing
hmm, right, decorators cannot "easily" change function names, right?
they can't control what name the function gets assigned to
@foo
def bar()
is syntactic sugar for
bar = foo(bar)
I don't particularly see this duplication as a huge issue (I have a bit of it myself), but if we're talking hypotethical fixes for this, I'd rather there be a way to register multiple concrete implementations for a set of overloads, which ends up extremely close behaviorally to functools.singledispatch (I understand the difference...) so it's kindof hard to justify
I was going to say next that registration seems like the obvious choice.
personally I think this is very different from singledispatch. singledispatch has a lot of limitations and it can easily get confusing. This is just a way to handle typing, and retain control of the dispatch
something like
@overload
def process(response: None) -> None:
...
@overload
def process(response: int) -> tuple[int, str]:
...
@overload
def process(response: bytes) -> str:
...
def process(response):
if response is None::
return process_impl_none()
if isinstance(response, int):
return process_impl_int(response)
return process_impl_bytes(response)
@process.overload_implementation(bytes)
def process_impl_bytes(response): # no types needed
...
hmm there is get_overloads now
you could get this out of typing.get_overloads() yes
I guess with the funny decorator that overwrites the signature of something + typing.get_overloads you could, but I don't think typing.get_overloads works statically does it?
it doesn't, but the point here isn't to work statically
what do you mean by "statically" here? in your example you could write something like:
def process(response):
overloads = get_overloads()
if response is None:
return overloads[0](None)
you're still writing the logic in the main process function, by hand, to decide which function in get_overloads to call
statically: type checkers being able to seeing the type
I see, yes you're right
I mean that's not really possible, afaics
get_overloads would have to have a tuple that changes as things are called on it
This is one of the things I definitely prefer in some other languages, and singledispatch (As well as things like plum-dispatch) don't quite fill the void
the return type of get_overloads would need be tuple[Callable[None, [None]], Callable[tuple[int, str],[int]], Callable[str, [bytes]]]
and similarly to keep changing as you add things
that seems theoretically possible with type checker special-casing
maybe, assuming you ignore "sketchy" things like writing overloads in other files or things of that nature
is get_overloads guaranteedd to return in the same order as they were registered?
So, here's a thought: it would be nice IMHO if overloads took an optional string argument that let it be placed in a dict, rather than a sequence
what would the key for the dict be? parameters are slightly annoying to be precise with given that there are 5 kinds of parameters
@overload("none"
def process(response: None) -> None:
...
def process(response):
ols = typing.get_overloads_dict()
if response is None:
return ols["none](response)
...
Overloads have to be contiguous, at least in the static type system
this would already be a lot less error prone
get_overloads() will do funny things if you don't follow that rule, I think we clear the list if the linenos aren't monotonically increasing, so the list doesn't keep growing if you reload a module
just a name. maybe it's just a string but in practical terms people are a lot less likely to get a name wrong that they chose than an ordering index
ohhh
I see you're naming them in the overload deco itself, thanks for that example
if isinstance(response, int):
typing.get_overloads_dict()["none"](response)
definitely has some smell to it
whereas typing.get_overloads()[1](response) ๐คทโโ๏ธ
and this is a pretty trivial, and backwards compatible change
anything without a name simply doesn't get entered into the dict
This seems viable and non-problematic to me, but I'm not sure how much support you'll get for it.
yeah I admit it's probably not some kind of crazy win
probably needs to come with spelling out some of the itnernal details to ensure it matches: #type-hinting message
what's the easiest way to float it?
typing category in discuss.python.org
but yeah I'm not sure I'd support it, the use case doesn't feel very common, the resulting syntax is not attractive, and it would need some type checker special casing to support
for what I'm suggesting, you don't need any special casing to support it
I guess you need special casing to make it type-safe
(Type checkers would have to infer some sort of TypedDict for the return of get_overloads_dict.)
you can get partial type safety by having _impl functions that duplicate the type signatures, and calling them
and also duplicating the type signature a third time
with that you still have type safety inside the implementation of the overload body
of course the type checker doesn't enforce that you actually obey the overload signature
sorry, when you say "with that" - are you referring to my idea, or the approach of calling _impl functions?
with the existing approach
you have it in my approach too? I don't see why that needs special casing
you call get_overloads_dict()["none"](None)
but get_overloads_dict()["none"] would be Callable[..., Any]
calling typed impl function retains type safety inside the implementation. calling the actual provided overloads would require type checkers to synthesize some knowledge (typeddict is likely the best canidate for it, but realistically, it could be something that is only statically allowed from inside the implementation so that type checkers can choose not to and special case it) for it to remain type safe in the body
you might want to take a look at this and search discourse for it a bit.
there's been discussions about better overloads/dispatch, and this was one of the libraries that has shown battle testing (and been brought up in these discussions)
okay, maybe the phrase "overload body" is confusing to me then -
@overload("none")
def process(response: None) -> None:
return None
this to me is an "overload body" and I don't see why it needs special casing to be type checked
but i'm not interested in the dispatch problem ๐คทโโ๏ธ
I guess I meant the overload implementation, the one that doesn't have @overload
right, so it has partial type checking only, provided that you are willing to duplicate the type signature a third time
actually, this started partially because someone was pointing out that you don't get proper type checking if you implement everything inline
@overload
def process(response: None) -> None:
...
@overload
def process(response: int) -> tuple[int, str]:
...
@overload
def process(response: bytes) -> str:
...
def process(response: None | int | bytes) -> None | tuple[int, str] | str:
# whole implementation here
this doesn't get type checked correctly, of course
yes it does?
well, then we differ in "correct" ๐คทโโ๏ธ
the type checker doesn't enforce that you return None on a none argument
right
but it does check the body of the type checker the same as it would for a non-overloaded function
I'm pretty sure pyright does, 1 sec
That's why I initially suggested to that person that they factor out individual implementation functions; process_none etc
because those functions can actually be type checked correctly
that would be pretty cool
it doesnt, but I could have sworn I've seen an error for this before from some tooling
๐ค
yeah. anyhow, so my point is I guess that I'm not asking with this change for any special support from the tooling - even without any special support from tooling, it's a trade-off
I think it does for contrained TypeVars, maybe you're confused with that
oh
hold on, I didnt notive pyright-play was stuck waiting for server
lemme load this up locally... edit: and yeah, isn't checked
- with my approach, the "dispatch" code does not get type checked at all
- but the bodies are type checked fully correctly, with no duplication of signatures
- with current approaches, you duplicate signatures 2x to get "partial" type checking, and 3x to get "full" type checking
why 3x?
because: once on the overloads, once on the _impl's, and once on the actual implementation function in union|ed form
@overload
def process(response: None) -> None:
...
def process_none(response: None) -> None:
# implement
def process(response: None | int | bytes) -> None | tuple[int, str] | str:
if response is None:
return process_none(response)
# impelment rest
the None appears three times
response: None appears three times, i should say
and note: this still isn't fully, fully type checked correctly
type checking the dispatch is imperfect anyway
sacrificing that type checking to avoid repetition while having fully type checked bodies seems like a good trade-off
hmm
I wonder if we Should specify that inside the body of an overloaded function, when the input type is known, any return must be consistent with the overloads
cause this is a tractable problem
I have a subclass of TypedDict
class InfoResponse(TypedDict):
# This class in total has around 40 keys
account: str
accountType: Any
country: str
clientIp: str
...
I would need another TypedDict for typehinting a return type of some function. The thing is that this new TypedDict will have same keys as InfoResponse but won't have all of it's keys. In fact it will also have some new keys. Thats what it would look like
class NewTypedDict(TypedDict):
# This class has around 39 keys
# clientIp from InfoResponse is removed
# accountType from InfoResponse is removed
# Those both keys don't exist anymore
account: str
country: str
region: str
# Rest of the keys from InfoResponse
...
# NOTE: "region" key doesn't exist in the InfoResponse
Im curious if I would have to rewrite ALL of those keys & types and place them in NewTypedDict or is there any faster way to create a TypedDict like this one?
I created a temporary "solution" by creating new classes that inherit from TypedDict
def __modify_info_response(name: str, base: Type["InfoResponse"], remove: List[str], add: Dict[str, Type[Any]]):
base_keys = {key: type_ for key, type_ in base.__annotations__.items() if key not in remove}
new_fields = {**base_fields, **add}
return type(name, (TypedDict,), new_fields)
Im open for any ideas if it needs some improvements (it probably does)
You can subclass typeddicts, so you can define the common fields in a base class and add the specific new fields in the subclasses
Are there usable alternatives to TypedDict in 3.6?
typing_extensions backports typing features to previous versions, but 3.6 is below the minimum officially supported version for Python or for the library so your experience is not guaranteed
I haven't thought of that
tysm 
3.6? ๐
my condolences
I'll probably conditionally assign to the type based on sys.version, if there are no alternatives. typing_extensions would be a good choice for the future, but for now... I don't get to change the dependencies.
And at least whoever set up the project chose 3.6. Default would've been 3.5...
Could've been worse!
ah yes, can't ignore unnecessary type ignore comment, because it's an unnecessary ignore, thanks pyright! ```
Pyright: Unnecessary "# pyright: ignore" rule: "reportUnnecessaryTypeIgnoreComment" [reportUnnecessaryTypeIgnoreComment]
you should get some good running shoes
just add an error to that line
๐
slightly offtopic to this channel but if i have an init declaration such as
class Error:
def __init__(
self,
code: str,
status: int,
message: Optional[
Union[FormattableString, LocaleStrings, str]
] = None,
*,
_property: Union[FormattableVar, str] = "global",
**attributes: Any
) -> None:
is there a preferred way to annotate this? im putting the * to block positional arguments past the message because i feel like the style of that would work well with message being a positional argument but im countering myself a lot on whether it actually is, especially considering message can be type string so it would be
Error("failed", 500, "it failed!")
which might look better as
Error("failed", 500, message="it failed!")
is there anything i can reference on where to use the *? personally i already always specify arguments with defaults with keywords in my codebase so adding these * wont require too much more work out of me, but the question is whether for usability/style to change every function to require arguments with defaults to be specified or to use the * tastefully (though thats kind of difficult to judge)
im translating much of the codebase into typescript which wont allow me to specify arguments in both ways and its probably easier to make every argument with a default specified by keyword like
class ErrorTemplate {
constructor(
code: string,
status: number,
{ message }: { message?: string | FormattableString }
) {
using keyword arguments is almost always better than positional ones
This is my compass, kinda ```py
fn() # chef's kiss
fn(a) # great, it's probably obvious what the argument means
fn(a, b) # sometimes it's obvious, sometimes not. use your judgement
fn(a, b, c) # a bit iffy.
# re.sub(pattern, repl, string) (or is it re.sub(pattern, string, repl)?)
fn_with_varargs(a, b, c, d, e, f, g) # as many as you want. though maybe consider just accepting a list
fn(True) # probably not, but there will be exceptions
fn(None) # probably not, but there will be exceptions
fn(a, True) # very likely bad
fn(False, True) # NO
fn(False, None, None) # you are fired
fn(a, ignore_errors=True, batch_size=4) # good
fn(a, ignore_errors=True, batch_size=4, <10 more options>) # now you have a bigger problem
@oblique urchin do you suppose the typing conformance suite could add support for run-time type checkers? I think it'd mostly need to add calls to functions defined in the modules, and .pyi files should not factor into the score, at least directly
frankly I was completely oblivious to the existence of the suite before the typing summit
is it possible to define abstract attribute somehow (not property)?
from abc import ABC, abstractmethod
class Base(ABC):
@property
@abstractmethod
def name(self):
pass
# Expression of type "Literal['test']" is incompatible with declared type "property"
# "Literal['test']" is incompatible with "property"
class Child(Base):
name = "test"
child = Child()
found it, it's not possible
For example, if an attribute is added dynamically during init. But thatโs why the type hint stuff is great, because it means you can have the cake and eat it too: Type hints /decorators only โ static checking Subclassing ABC โ uses type hints / decorators to automatically generate runtime checks Generally not, in many simple cases you ca...
this is not really a good example. you're defining a class attribute instead of an instance method so it will fail the type checker regardless of whether it's abstract
if you want to have an "abstract attribute", consider using Protocols to specify the interface that's needed. this separates the interface from the implementation
that actually works, thank you
from typing import Protocol
class BaseProtocol(Protocol):
name: str
class Base(BaseProtocol):
# name = "25"
pass
# Cannot instantiate abstract class "Base"
# "BaseProtocol.name" is not implemented
child = Base()
though it's a bit annoying that this code doesn't show any issues
from typing import Protocol
class BaseProtocol(Protocol):
name: str
class Base(BaseProtocol):
# name = "25"
pass
is there way to get similar error defining the class, not instantiating it?
Try using __init_subclass__
I wouldn't be opposed but I'm not sure what it'd look like and I think runtime type checkers have a pretty diverse set of interfaces
if have an c extension module and you want to add type hints for it, do you just need a module_name.pyi file, or do you need a py.typed file too?
I saw this but it only has pure python so I wasnt sure https://dev.to/whtsky/don-t-forget-py-typed-for-your-typed-python-package-2aa3
__init_subclass__ can help ensure that subclass will have certain property but it doesn't help in showing a type hint if it doesn't
I can only speak for my own project, but typeguard's import hook based instrumentation would handle that fairly easily, the test suite just needs to call the functions to trigger the checks
IIRC Cecil is working on a similar mechanic in Beartype if it's not there already
yes, you would need both
I think both are necessary.
Ope, didn't see the other response. My bad.
Py.typed is only not required if the package is named types-xxx
oh interesting ok
how can I put the comment to surpress the warning above the line, instead of on the side?
cuz then I get the warning of "line too long (pep8)"
I never understood the whole point of the py.typed file
is it only to say to the linter that its type-hinted so to analyze it?
for example pandas dosent seem to have it: https://github.com/pandas-dev/pandas/tree/main/pandas
Yeah. If it doesn't exist, mypy doesn't even bother reading the package.
that seems like a bad design on mypy's part
why dont they scan the package one time when its installed
though pandas provides .pyi files in pandas/_libs
and mypy will read them if there is no py.typed file?
the original reasoning was that mypy may crash on untyped code, so it should only look at code that is meant to be type checked
I'm not convinced this is still a good reason
And for the record, pyright always type checks all packages, whether a py.typed marker is present or not
i think type checkers should still issue diagnostics when importing from packages without py.typed (so you know you're getting shitty types), and then go and do the best it can
turn_types.py
import importlib
import pathlib
import sys
lib_names = sys.argv[1:]
for lib_name in lib_names:
try:
lib = importlib.util.find_spec(lib_name) # type: ignore[attr-defined]
for loc in lib.submodule_search_locations:
typed_file = pathlib.Path(loc) / "py.typed"
print(f"{loc=}, {typed_file.exists()=}, touching file")
typed_file.touch()
except Exception as err:
print(f"{err=}")
continue
- python3 turn_types.py requests dill django_filters setuptools
my favourite hack
without effort to install and keep up with stub libs ๐
:incoming_envelope: :ok_hand: applied timeout to @rustic gull until <t:1717086164:f> (10 minutes) (reason: duplicates spam - sent 4 duplicate messages).
The <@&831776746206265384> have been alerted for review.
I think there's good reasons against this.
Allows a project to incrementally add types to things internally without the issues of that being exposed to it's users until they are ready. Very much works well with the spirit of gradual typing
py.typed says "We're ready enough to take bug reports from our users for types now"
Which is why .pyi files exist for public apis.
Am I wrong to expect that reveal_type(bar) in the below example should yield str instead of str | None? (pyright, if it matters.)
def foobar(foo: str | None, bar: str | None):
if foo is None and bar is None:
raise ValueError("foo and bar can't both be None!")
if foo is None:
reveal_type(bar) # Expected `str`, got `str | None`
I mean you're wrong in the sense that you're expecting too much
Yes, for example if bar is not none
like, obviously control flow like that can be arbitrarily complex - how much do you expect the type checker to work through?
Oh I'm missing the logic
yeah
I'd expect that to narrow
the problem is that basically, the more complex you make the narrowing/auto-casting logic, people will depend on it in progressively more complicated situations, and then it can break in more surprising ways when it does break - either the logic actually changing so it's not guaranteed, but worse, eventually you hit the "limit" of how much logic the type checker processes
Yeah, that makes complete sense, I'm just kind of used to how well the narrowing normally works
it's also inconsistent with how any mainstream statically typed language works - languages that have smart casting (Kotlin) or pattern matching (Rust) also have this "limitation" - you can hit points in the code where its logically guaranteed that a type has been narrowed in a certain way, but the static type system isn't aware of it.
But I guess narrowing based on multiple different variables is a bit of a jump
yeah, I think usually the narrowing works purely off of that variable being mentioned in the condition, though there could be counter-examples to that
It's possible to handle any amount of chaining of things that verify a type across any number of values soundly
well, to do that you need a rigorous theory of what counts as a "static" expression
it isn't possible for arbitrary expressions, but for combinations of conditionals which have narrowing behavior, it is. We have a definition of such conditionals since the acceptance of TypeIs
you could do it for this case and similar cases - the question is whether the benefits are worth the downsides ๐คทโโ๏ธ
There aren't that many downsides here, and quite a few benefits to having narrowing behavior follow logically from control flow, and there are some open discussions right now, some recently accepted things, and some in progress specification updates that either would benefit from or contribute to better narrowing from control flow.
you're right that we would need to be careful to be clear on the rules of what could narrow like this is to avoid it being unclear to users or being broken though
main benefit is when people write "obvious" code, the type checker that's supposed to be good enough to be checking this over your shoulder for you doesn't tell you to write less obvious (And worse!) code making people feel like they are fighting with tools meant to help them
๐คทโโ๏ธ I just don't agree; it's easier IMHO when the type inference rules are very simple.
i don't think it's a coincidence that other languages with similar features also don't implement this.
@hardy linden the actual way to go here probably, is to use pattern matching, I would think.
I haven't used it much in python, but that's how you'd do similar things in other languages and get the "right" type automatically
Hello, I want to have function that will have similar signiture to loop.run_in_executor
mypy is able to detect when I pass function with args that are different from args that I passed along with given function.
I am able to achieve similar behavior with ParamSpec but this need both args and kwargs.
Can you advice what typing feature I need to use to have function that will be checked and fail the same way like loop.run_in_executor in example below?
import asyncio
def x(a: int, b: str) -> None:
pass
async def main() -> None:
executor = ProcessPoolExecutor()
loop = asyncio.get_running_loop()
loop.run_in_executor(executor, x)
""" mypy is able to detect that int and str args are missing
mypy error:
Argument 2 to "run_in_executor" of "AbstractEventLoop" has incompatible type "Callable[[int, str], None]";
expected "Callable[[], Any]"
"""```
Perhaps you can check the type definitions for loop.run_in_executor, and copy that? Otherwise, could you explain the issue that you are facing right now?
It's Unpack
With TypeVarTuple
Honestly I'd just accept a Callable[[], T] with no arguments
if you want to pass arguments to an existing functions, use a lambda or a partial
pass```
works like a charm, thanks
It looks like you're on Python 3.11+, which means you actually don't need Unpack here at all, you can just use:
def run_in_executor(func: Callable[[*Ts], None], *args: *Ts) -> None:
pass
Unpack[Ts] is the backwards-compatible version of *Ts, for versions of Python where you can't yet use * in annotations
not always possible, I want it for example as a wrapper for creating new processes def create_process(target: Callable[[*Ts], Any], args: tuple[*Ts], daemon: bool) -> multiprocessing.Process: return multiprocessing.Process(target=target, args=args, daemon=daemon)
partial will work here
(functools.partials are picklable)
of course if you want to respect some existing API, you don't have a choice
Is it possible to do postponed evalutations of annotations
Ie.
class Test:
x: list["Test"]
but for actual typing.Annotated?
So like how to make
class Test:
x: typing.Annotated[list["Test"], something]
work?
from __future__ import annotations
isn't that only required if you are on <3.7?
this is impossible if you are on <3.7
on >=3.13 this should no longer be required
on <3.13 just slap from __future__ import annotations in literally every file, it wont hurt
haha ok sure
this seems to ruin my code which is inspecting the __annotations__ attribute of the class
with the future import of annotations it seems to be changing the types to strings (when I loop over the .items() of this dictionary
which, looking at the PEP is intended...
but I am using that info...
anyway, there is a function in typing module that gives you typehints for an object
!typing.get_type_hints
!d typing.get_type_hints
typing.get_type_hints(obj, globalns=None, localns=None, include_extras=False)```
Return a dictionary containing type hints for a function, method, module or class object.
This is often the same as `obj.__annotations__`. In addition, forward references encoded as string literals are handled by evaluating them in `globals`, `locals` and (where applicable) [type parameter](https://docs.python.org/3/reference/compound_stmts.html#type-params) namespaces. For a class `C`, return a dictionary constructed by merging all the `__annotations__` along `C.__mro__` in reverse order.
The function recursively replaces all `Annotated[T, ...]` with `T`, unless `include_extras` is set to `True` (see [`Annotated`](https://docs.python.org/3/library/typing.html#typing.Annotated) for more information). For example:
oh haha ok thanks
use this instead of .__annotations__
3.14, not 3.13
ok... I have ran into another maybe more obscure problem...
So this will fall over:
from __future__ import annotations
from typing import Annotated, get_type_hints
class Student():
name: Annotated[str, 'some marker']
def test(self):
for k, v in get_type_hints(self, include_extras=True).items():
print(k, v)
print(get_type_hints(Student, include_extras=True))
Student().test()
Commenting out the top line will work but then I will lose the ability to have forward-referencing
Sorry, I should have specified that it raises this error:
"typing.py", line 551, in _evaluate
eval(self.__forward_code__, globalns, localns),
File "<string>", line 1, in <module>
NameError: name 'Annotated' is not defined
which originates from the get_type_hints call inside the test method
the get_type_hints call works just fine when called from the second from last line both times
passing an explicit globals() arguments to get_type_hints() may fix this
I tried this but then I think I lost the forward-referencing as it then couldn't find the class itself...
actually, I think it works when the code is in the same file, but in my case I am defining this in a subclass which is then used in a different file
from __future__ import annotations
from typing import Annotated, get_type_hints
from tst2 import Base
class Student(Base):
name: Annotated[list[Student], 'some marker']
print(get_type_hints(Student, include_extras=True))
Student().test()
And Base as a class just has
class Base():
def test(self):
for k, v in get_type_hints(self, globalns=globals(), include_extras=True).items():
print(k, v)
this raises the error NameError: name 'Student' is not defined
Again, this works when calling from outside the class, but not inside
try get_type_hints(type(self)) instead of get_type_hints(self)?
Say I have
t1: tuple[int, ...] = ...
# based on some external conditions I now know that t1 is tuple[int, int]
def foo(t: tuple[int, int]):
...
foo(t1) # tuple[int, ...] incompatible with tuple[int, int]
Is there any clean way to change the type of t1 without a # type: ignore?
cast?
TypeGuard?
But it will be a different kind of... future, right?
that's not the current plan
this will be the future but not the __future__
the current plan is just to implement it in 3.14 as the default semantics
Yeah, as usual my attempt at inserting a pun made my message unclear
the future will look like this: ```Python 3.14.0a0 (heads/pep649-inspect:11a96f9a61, Jun 1 2024, 08:54:47) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
def f(x: undefined): pass
...
f.annotations
Traceback (most recent call last):
File "<python-input-1>", line 1, in <module>
f.annotations
File "<python-input-0>", line 1, in annotate
def f(x: undefined): pass
^^^^^^^^^
NameError: name 'undefined' is not defined
undefined = int
f.annotations
{'x': <class 'int'>}
Just looked it up, seems like a cool feature, but then I assume I have to define a function and run it through that? Seems a little tedious
Well what are your external conditions?
I don't have a concrete example (it was just floating around in my head) but if you need an example, maybe something like checking if a degree of a bezier curve is 2
(which would happen a lot in one of my programs)
bez_deg = 2
def calc_bezier_property(n_deg_bezier) -> tuple[tuple[float, ...], ...]:
...
def do_stuff(second_deg_bezier) -> None:
...
for curve in calc_bezier_property(...):
if bez_deg == 2: # would I use a TypeGuarded function here?
do_stuff()
What are the possible values for bez_deg?
Pretty sure you can do if len(curve) == 2: do_stuff(curve)
iirc, a type-checker like pyright can narrow the type of a tuple based on a len() check. Would help here if so.
EDIT: this works with the original example at least โ
t1: tuple[int, ...] = ...
def foo(t: tuple[int, int]): ...
if len(t1) == 2:
reveal_type(t1) # Type of "t1" is "tuple[int, int]" - Pylance
foo(t1) # valid in the eyes of the type checker
how should I deal with mypy complaining about missing return?
from pathlib import Path
from shutil import which
def exe(app: str) -> Path:
if executable := which(app):
return Path(executable).resolve()
else:
Exception(f"Executable not found in PATH: {app}")
you need to use the raise keyword to actually raise an exception
glad to help xD
haha i cannot believe that's the mistake i was making
whats the progress on higher kinded types support, maybe there's a thread with discussions about it or something like that? is that even something thats being worked on?
Is there way to ensure type checkers will assume some function's __doc__ is a str and not str | None if doc-string is present?
from typing import assert_type
def test():
"""hello"""
pass
# shouldn't raise an error
assert_type(test.__doc__, str)
# shouldn't raise an error
s: str = test.__doc__
Why though
Also, there's this
$ python -OO
Python 3.12.0 (main, Oct 10 2023, 20:10:02) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def foo():
... """Hmm"""
...
>>> foo.__doc__ is None
True
>>>
to reuse the doc-string elsewhere?
๐ค
why not just handle the case where it is None?
sure, it's possible to just cast to str for every doc
You can just do func.__doc__ or ""
oh, that actually simple enough, thank you
as far as I know nobody is working on this
Is there a Python 3.10+ compatible way to annotate *args and **kwargs? For annotating **kwargs I was going to use def func(**kwargs: typing.Unpack[TypedDictInstance]) but saw that Unpack is not in 3.10.
use typing_extensions.Unpack
so this relates to another question I'm confused about. Do I need typing_extensions if I'm going to only run a static type checker on python 3.12, but need the code itself to run on 3.10?
if you're importing it at runtime, yes
can I avoid that by doing if typing.TYPE_CHECKING: import typing.Unpack? ...but then I suppose I sould have the unresolved Unpack in the function signature
you can, but you also would need from __future__ import annotations to prevent NameErrors
Can you do **kwargs: "*TypedDictSubclass"?
is that "legal" code? ...like, would type checkers verify that?
doesn't look like it ๐ฆ
Weird I'd have expected it to work
afaik, it's not being pushed or developed beyond one mypy plugin made a while back.
does it work without "" ?
nope
then the problem is probably that your python version is too low, and pyright doesnt allow this syntax, even in strings
library I maintain conforms to NEP-29, so going to be a bit before I can bump it up to 3.11 ... considering the lack of annotations elsewhere in the library, we probably will just work elsewhere in the library and circle back to *args and **kwargs usage when we are at 3.11 as a minimum
You could make a placeholder for Unpack in your else block so that it's a viable runtime annotation.
Your case isn't quite what this example is for, but something like this (https://discuss.python.org/t/using-get-type-hints-with-cyclic-imports/44828/6) might serve your needs in a way that doesn't break at runtime while making sure your type-checkers understand your intent at type-checking time.
chevapchichi
Technically it would be **TypedDictSubclass, but I think that was rejected or postponed or something
because mapping unpack vs iterable unpack
They are both unpacking so Unpack makes sense, but just doing a single star on kwargs type doesn't make sense
How useful is adding type hints to *args and **kwargs? Type checkers seem to raise warnings about missing arguments anyway, based on where they are used, though sometimes as false positives.
Nope it's still one star for both
it is currently only Unpack for typed dict
and possibly in the future **
Oh I see
quite important
as important as typehinting any other parameters
In what way are they important? What does adding them change? Is there an example of how to type hint them I can follow?
I think it's useful, the alternative is I have a bazillion keyword args specified in my function signature
Well it depends what you do with them. Sometimes you need paramspec sometimes you need unpack with a TypeVarTuple and sometimes you need unpack with a typeddict
https://pyqtgraph--3039.org.readthedocs.build/en/3039/api_reference/graphicsItems/plotdataitem.html#pyqtgraph.PlotDataItem I mean, I know all the keyword arguments and the types getting directed to ``**kwargs`
Sometimes you need to call super with passthrough args and kwargs and that's just not possible yet
(scroll down a bit to see the tables of recognized keyword arguments)
(personally I hate how **kwargs is used in the library here, but this PR is about documentation, not looking to change things up too much)
I would say the most common way of typehinting *args is homogenous
Is one of the links supposed to be a GitHub issue?
Mm. Sounds like TypeVarTuple would be the way to go if you care about how the positional arguments. Haven't really had those cases yet.
The main use case for me is indeed forwarding calls, most frequently in __init__.
No, a link to the docs built as part of a PR Iโm making to upgrade the documentation on this class and add a bunch of type annotations.
I posted it as all the expected arguments for **kwargs are listed
Ah I see. It certainly has quite the number of arguments.
Default pylint settings would not be happy.
Considering I got sphinx to build w/o warnings, numpydoc to have no (relevant) errors, I'm happy with that ๐ definitely not looking to add pylint into the mix ๐
this is indeed an older codebase tho... going to be a while before I can fully modernize it; will be tough to do without breaking some of the pulbic API :/
Does anyone know why I should use
type UserId = int
over
UserId = int
For simple aliases like that it doesn't make too much difference, though it makes your intent more explicit
The advantage of the type statement is that it lets you have explicit generic parameters and use forward references
https://github.com/python/typing/discussions/1761
"Self type for a method parameter is an automatic LSP violation"
Doesn't that defeat the whole point of the parameter
And surely you can say the exact same thing about a type var bound to self?
Yep. It has exactly the same problem.
It's okay in the return position, but not as a parameter (ignoring the self parameter)
why do people go trough the hassle of making type-hints backwards compatible (i.e. using typing.List, or using typing_exstensions) when you can just use future imports (from __future__ import annotations) to avoid evaluating them on runtime?
(and you can hide declarations and imports under if TYPE_CHECKING: ...)
Thee's a few reasons:
- People may want to support runtime type checkers, which won't work well with the future import
- Some types appear outside of annotations (e.g., in cast, TypeVar bounds, etc.) and aren't helped by the future import
- Some people may still support versions of Python that don't have the future import, or haven't updated their practices since they dropped support for those versions
People may want to support runtime type checkers, which won't work well with the future import
so if I create a library that uses future imports, will the user not be able to use runtime checkers with other libraries because my future imports disabled it?
Possibly; if the runtime type checker jumps through enough hoops it might still work
interesting
first time I hear about that
I think MCoding said smth along the lines that you should always use future imports
I don't know who that is
People may have different opinions based on what they use annotations for
pairing "always use the annotations future import in files with annotations" with "always have imports that can be resolved at runtime" is an actual guideline I argue for, and enforce in a relatively large codebase. The first without the second creates some problems for runtime introspection. Jelle is right that some parts of that might not matter if your code never interacts with other code that uses annotations in a way you aren't aware of.
anybody try to use typechecked from typeguard? what you are using to check if your types are correct ๐ ?
Why do you want runtime checking?
In short, you can only really "check" trivial types like str, int or str | bool
If you're using a type checker reasonably well, you shouldn't really encounter issues when you provided a boolean where a string was expected
thanks @trim tangle ๐
That's not really a limitation of the tools though, it's kind of a fundamental problem with runtime checking. It's not possible to check whether a function conforms to Callable[[int], str] at runtime
And everything else stems from that (e.g. Iterator[int] is pretty much a Callable[[], int])
Why is tuple[Literal['a','b'], *T] not considered equivalent to tuple[Literal['a'], *T] | tuple[Literal['b'], *T] (in an invariant position)? Surely those types should always be equivalent?
Can you show more code perhaps?
also what is T?
This is my exact return type: list[tuple[Literal["tile", "wall"], str, str, int, int, int]]
This is the code that creates the returned object:
return sorted(
[
*(("tile", *tile) for tile in best_tiles),
*(("wall", *wall) for wall in best_walls),
],
key=lambda tile: (r - tile[3]) ** 2 + (g - tile[4]) ** 2 + (b - tile[5]) ** 2,
)[:5]
This is the exact error:
Expression of type "list[tuple[Literal['tile'], str, str, int, int, int] | tuple[Literal['wall'], str, str, int, int, int]]" is incompatible with return type "list[tuple[Literal['tile', 'wall'], str, str, int, int, int]]"
"list[tuple[Literal['tile'], str, str, int, int, int] | tuple[Literal['wall'], str, str, int, int, int]]" is incompatible with "list[tuple[Literal['tile', 'wall'], str, str, int, int, int]]"
Type parameter "_T@list" is invariant, but "tuple[Literal['tile'], str, str, int, int, int] | tuple[Literal['wall'], str, str, int, int, int]" is not the same as "tuple[Literal['tile', 'wall'], str, str, int, int, int]"
Consider switching from "list" to "Sequence" which is covariant
I just factored out equivalent tuple members because I didn't feel it was important
If you have e.g. list[str | int], that's not the same as list[str] | list[int]. The first one can mean ["a", 42, 3], while the second one can't
Yes, but I'm making a union of two tuples, a single member of which differs, which should be equivalent to a tuple, a single member of which is a union
Hmm yeah
This seems like a bug, you should report it to the pyright repo
Also... maybe use dataclasses or NamedTuple if you want to differentiate between different cases. Plain tuples with literals will have a lot of these inference issues
Is there any typing documentation that covers type hinting various dunder methods for classes?
I looked at both typing and mypy docs but couldn't find anything
Mypy did cover a few like init and slots but I'm interested in the rest like comparators, hash, etc
there isn't much to say in general, each is different and mostly the types flow from the general meaning of the dunder
my autotyping tool can automatically add annotations to some, so that could be helpful: https://github.com/JelleZijlstra/autotyping?tab=readme-ov-file#usage
and you can look in typeshed for how these dunders are annotated on standard library classes
Thank you, I'll take a look at both
The CPython data model docs aren't geared specifically towards typing users, but they often say things like "this dunder method should return a bytes object" etc. https://docs.python.org/3/reference/datamodel.html#special-method-names
Thanks, figured some stuff but I still have some doubts
For example, I looked at a bunch of code and found slightly varying type hints for __eq__
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Path):
return NotImplemented
return self.file.resolve() == other.file.resolve()
def __eq__(self, other: object) -> bool:
if isinstance(other, Path):
return self.file.resolve() == other.file.resolve()
return False
from what i can gather, returning NotImplemented is the "correct" way from data model docs
but the type hint only indicates bool?
mypy isn't complaining so i guess it's fine
__eq__ is tricky. I think object is generally the safest annotation
the type system generally ignores NotImplemented (we pretend it's Any)
That explains it, thank you
Yeah, for binary-operation dunders, we generally do the type hint "for the operator the dunder is implementing" rather than strictly trying to type hint the actual return type of the dunder itself. So whereas int.__lt__(42, "42") will strictly-speaking succeed at runtime (it just returns NotImplemented), 42 < "42" will not succeed. So instead of trying to faithfully represent in the stub that int.__lt__ sometimes returns NotImplemented, we simply don't allow int.__lt__(42, "42") as a possibility. Because, as Jelle says, type checkers don't really understand NotImplemented.
How do people validate schema on inputs/outputs. Ex: my function accepts a dataframe with columns A, B, C (and no others) and produces an output json structured as {A': str, B': list[str], C': "tz aware timestamp"}? (not sure if this should be in #type-hinting but it seems fairly close)
is the concept of type constructors well-defined in python from the type system side?
I'm sorry that the higher-kinded types PEP was stale for a long while, but I'm revisiting it from time to time and can't give an exact answer to the question above
one of the examples, if you don't mind the syntax is python def magic_map[T, U, I: Iterable[T]](function: Callable[[T], U], iterable: I[T]) -> I[U]: return type(iterable)(map(function, iterable))
it feels like we need to work with __new__ rather than __init__?
i.e. some way to define/check that the constructor can be called with Iterable[T] and will return Self[T]
(iirc parametrizing Self was already discussed as a part of HKT)
the idea is still pretty raw to put it somewhere like python discussions
is it I: Iterable[T] and not just I: Iterable? you're parametrizing it later
hm, that's fair yeah
but I'm moreso worried about backwards compatibility
specifically Self
and this is the main cornerstone
im not sure if just type(thing)(...) is even a good idea if you bind stuff like Iterable - its not guaranteed that you can make it from another iterable, yeah. i was thinking of something more like Iterable + From[Iterable] maybe? but.. uh, thats an intersection type or something
that is in fact an intersection type
though basedmypy already implemented generic typevars to some degree
hmh, maybe...
i never implemented type systems but in my mind it works like:
def f[A, B, I: Iterable, some_syntax_for_a_constraint(From[[Iterable[B]], I[B]])](I[A], (A -> B)) -> I[B]:
...
# (the From is like, detailed..ly? parametrized because its not guaranteed that the I constructor is generic over all iterables, but it could be just for B and should still work.. is that even a thing in python? i guess thats ad-hoc polymorphism, and typing.overload is kinda that?)
f(list[A], (A -> B))
list: Iterable # I: Iterable, satisfies
list(Iterable[B]) -> list[B] # satisfies From[[Iterable[B]], I[B]] constraint
# ->
list[B]
# maybe even...
f[A, B, C, I: Iterable, From[[Iterable[B]], I[C]]](I[A], (A -> B)) -> I[C] # ?? if I(Iterable[B]) is not I[B] but I[something else (C)], like, fully expressing the type(xs)(Iterable[B])
# in that case..
f(list[A], (A -> B))
list: Iterable # ^
list(Iterable[B]) -> list[B] # -> C = B , but could be not that
# ->
list[B]
# and what about cases when, let's say
class T[A]:
...
class U[A, B](T[A]):
...
def f[A, B, C: T](C[A], (A -> B)) -> C[B]:
...
f(U[A, V], (A -> B)) # well, U: T, U[A, V]: T[A], but it also has the V part, what to do with it? leave it the same? so
-> U[B, V] # ?
Trying to type this beauty in the absence of PEP-661. Client code is fix.
class _MissingInt(int): pass
_MISSING_INT = _MissingInt()
class _MissingStr(str): pass
_MISSING_STR = _MissingStr()
class C:
x = 0
a = ""
def __init__(self, x: int = _MISSING_INT, a: str = _MISSING_STR) -> None:
if x is not _MISSING_INT: self.x = x
if a is not _MISSING_STR: self.a = a
# client code
c = C(1, "a")
assert c.x == 1 and c.a == "a"
c.x.as_integer_ratio # these need to pass
c.a.casefold
class D(C):
x = 1
d = D(a="a")
assert d.x == 1 and d.a == "a"
How horrible is this? I'm out of ideas otherwise.
you can just do
_MISSING: Any = object()
class C:
x = 0
a = ""
def __init__(self, x: int = _MISSING, a: str = _MISSING) -> None:
if x is not _MISSING: self.x = x
if a is not _MISSING: self.a = a
...
class C:
x = 0
a = ""
def __init__(self, x: int = _MISSING_INT, a: str = _MISSING_STR) -> None:
if x is not _MISSING_INT: self.x = x
if a is not _MISSING_STR: self.a = a
# why not this? :
class C:
def __init__(self, x: int = 0, a: str = "") -> None:
self.x = x
self.a = a
^ as well if that's reasonable, but I've seen situations where it isn't and examples given to avoid sharing confidential stuff end up leaving out why the "obvious" solution can't work.
This seems to work and is so much better. I missed the obvious Any.
I would've made it this way, but the default values are class attributes in the client code, and they will change with subclasses.
Hi, I wanna make that "hello world" program i saw on instagram, basically all the alphabets keep printing, until the word's letter matches, then that letter stays and next letter's column same alphabets keep printing till that letter matches
basically I want this out put for the word "ice":
a
b
c
d
e
f
g
h
i
ia
ib
ic
ica
icb
icc
icd
ice
ice
how to do this?
This doesn't seem related to type annotations, you should see #โ๏ฝhow-to-get-help and make a help post in #1035199133436354600
ah oke sry ;-;
How is that related to hello world??
Pywrong
@trim tangle ello
we ready to cook?
perhaps
I am not completely awake yet
mh, its hard to discuss about seriously writing something, im not sure what to start with
who are we writing for (what knowledge of python we can assume of readers)? what should we cover? with what do we start? how to make examples that would not feel useless?
i think too much yapping at the start like i did in thing will likely make people quit because it starts with some useless for them at the moment stuff
but with what to start so it wont feel like it? how to even introduce typehints to people who havent used them before? maybe make an example of untyped code, and show that by using typehints the typechecker can help us figure out some errors?
Have you seen https://diataxis.fr/?
nope
I think this structure would make the most sense:
- a "central tutorial" that reads like a book chapter and gets you from 0 to basic type checking, including:
- a brief explanation of type annotations
- setting up your editor and/or running a CLI type checker
- annotating function parameters and return types
- a bunch of self-contained how-to guides for specific topics, aimed at people who already understand the basics (e.g.: how to type a function decorator)
- branching tutorials/explainers on more complex topics (like the generics section I already have)
hmh
what about like, builtin types? where would that go? people are obviously familiar with int and str or whatever, but lets say we want to introduce list or dict, and they have generic params
do we explain generics, or at the start just say "list[T] means list of T's" or something? or would that only be in the "generics" section?
Basic stuff like list and dict will probably be in the initial tutorial
Actually maybe it's better off like this:
- have a main "tutorial"
- have subsequent "tutorials" on broader topics, like generics or protocols or whatever
because some topics are not that self-contained and might benefit from an overarching narrative structure
Because this is very basic material, we can just say "list[int] means a list of integers <insert some examples>" and not go into more detail
i think thats kinda what i tried to do in https://lambda-abstraction.github.io/lambda-abstraction/python-typehints/builtin-types/ and it seemed like a good idea
I think the mainland tutorial should cover:
- using classes as types (like
strandintandYourOwnClass) - unions (especially
X | None) - basic built-in generics (
list,dict,set)
yeah that seems good so far
regarding uh, being a "modern" (guide/tutorial), would we use python3.12 syntax for typevars? should we then still explain typing.TypeVar? a lot of stuff still uses those
I think we should ignore the 3.12 syntax completely for now
hmh, fair i guess, although i really like it, keeps the scope of typevars clear, specially nice for type aliases with the type keyword
Maybe we'll figure it out by the time the decision is needed
maybe we'll give up after int
"int means int, but, well, bool is also an int, here is liskov substituion principle, uh, inheritance, yes, thanks, we love subtypes"
Don't forget about int being a subtype of float
even though issubclass(int, float) is false
mh, alright, do you have a repo or something? how would we handle the uh, collaboration aspect? does maybe using something like liveshare make sense in the case of just writing "articles"?
would we probally need some consistent style guide? perhaps about that we should worry later
I think I'm going to post it on https://decorator-factory.github.io/typing-tips/
I think I'll try making a very brief draft of the mainland tutorial
we could both make a couple drafts and maybe merge them together incase some information missing
yes
alrighty ill try to cook something
what are you doing? writing a book about typing?
sort-of? i think its more like a tutorial mostly intended for people completely new to typehints
imo, mypy's cheatsheet is perfect, even for newbies
but it doesnt explain why use them & what do they mean, and its well, a cheatsheet, not really structured - just a bunch of simple examples and thats it
Yeah, it's a cheat sheet, not an introduction
thing
i started writing something i guess, im not sure of the order of things here though, and specially the "wording" of stuff, im not really.. a writer
haha i love how we both included the "other developers include yourself"
hmh, using screenshots is nice
If you're in VSCode, you can Ctrl+V an image into a markdown file and it will just work
hmmh, the re.Pattern example is a bit weird for the first introduction to me
although i dont know what first example would be good either
yeah probably
Maybe
def find_match(pattern: str, strings: list[str]) -> tuple[int, str] | None:
and then do something like find_match(b"[0-9]", ["100", "hmm"])
i think something simple as
def double(number: int) -> int:
return number + number
print(double("42"))
might be a bit easier? with this one you're introducing generics & unions right away, sort of a big jump from zero
the
Error checking
Type hints don't do anything at runtime: you're free to call find_match(42, socket.socket()) and get a nasty error like TypeError: 'socket' object is not iterable.
still applies good here, just in the other way - you can typehint something too strict / close minded, that passing a valid thing wont work
with the last point you could specify then that you'd want to typehint arguments as generic as possible, while keeping the return type as specific as possible
thats a good rule to go by when typing functions imo
Maybe something with two arguments, like ```py
def censor(string: str, stars: int) -> str:
return string.replace("java", "*" * stars)
I also want to reuse this example for autocompletion
Well, this is kinda mean to Java
i think its funny, and articles being funny makes them like, more fun to read to readers
Well, maybe it's not that funny if you love Java
I think I'll go this route
I like this example because it showcases a bunch of things at once. There are more basic examples later
hmh now when i think of it
perhaps it is fine? the "target audience" knows that a list can store different things, and that a function can return different types, so thats basically just showing most of the syntax they'll need in one go to express those things
Yeah the re.Pattern[str] part is kinda stinky
hmh, i think those changes + a small explanation of subtypes would make a good introduction
What about subtypes?
class Dog:
...
def create_dog(height: int) -> Dog:
dog = Dog()
dog.grow(height)
return dog
something like this example, but with the animal being a parameter, and a common Animal parent could work, so people would understand they dont need Cat | Dog, just Animal
I think that's good material for the next chapter
hmh
the typing docs already use 3.12 syntax
type var defaults are still not in any released python, so if you do use pep695 syntax in your tutorial, you'll need to switch a lot of gears when explaining defaults
fair
(inheritance is not used that often)
maybe an int | float example? common too
Remember, this is supposed to be the first thing someone reads about type hints
int | float is a very particular... quirk
yeah but people use numbers frequently dont they? like int(input()) and float(input()), would make them think they need int | float, maybe a !!! note would be good
Using int | float isn't actively wrong, I think. It will work
It might be good to mention this in one of the sequels, as i said
Thereโs two giant recent threads on float | int in the Python discourse forum under the typing category, if yโall want reference for that.
There's more stuff to cover, like annotating stuff in classes, dataclasses, Callable, other abstract classes from collections.abc and why list[str] is usually a bad parameter annotation...
Love to see mypy complain about redundant unions
python/mypy#17223
Itโs a bit of a weird one, where float isnโt technically a super type of int, but more like โsyntactic sugarโ for float | int.
At least, from a type checkerโs perspective.
They just just make the runtime catch up with the type system here IMHO
How do you mean?
was the first just supposed to be a should? would make sense then probally
Btw, this would highly benefit from a review by a native English speaker. Because the writing is shit
If you mean that float and int should be completely compatible by having the same attributes and methods, I donโt disagree.
The status quo is more confusing than it already is otherwise.
||are we both russians writing an article in english||
Currently, hex and fromhex are missing, iirc?
Yeah, that.
And a type checker wonโt warn you of that in many cases.
I don't know what's worse honestly, int.hex missing or (42).hex() returning 0x1.5000000000000p+5
Wouldnโt it make more sense to say type-hints are informal documentation?
As opposed to formal?
At least for me, I imagine formal being a docstring following a specific style guide.
typehints follow a certain style too dont they
"informal documentation" is something like "call this function with a string"
it's formal because type hints have precisely defined meaning, as opposed to arbitrary English words
That is sorta what a str type hint on a single-parameter function would mean though, but fair enough.
A docstring can also contain formal documentation, like
:type foo: str
:type bar: str
(or whatever format you prefer)
This might be a personal opinion thing for me, where if there isnโt any prose in the documentation, it feels informal. Regardless, that was my attempt at a minor nitpick. Iโll read the rest 
ill try to write something on collections.abc & being generic in parameters & strict in returns
should abcs and protocols be in one page? maybe next to each other? they kinda deal with the same problem (when typing, atleast)
Just curious, would you guys consider contributing this guide to the official typing page, or are you set on it being entirely independent? Feels like it would be a good fit for the first entry in the โGuidesโ section on the front page.
I think we should separate the "teaching the basics and the fundamental concepts" stuff from the "explaining individual items and their quirks" stuff
E.g. we could make an article that explains how and why you can define "interfaces" to accept types you don't directly know about. It could use Protocol, probably.
And then we could have a separate article in the style of object vs any to explain ABC vs Protocol
but you did say "other stuff to cover: abstract classes from collections.abc"
is this an idea you had just now
I meant using Sequence, Iterable, Collection instead of list
yeah
well, we don't need to explain ABCs to show how to use Iterable
fair
And these ABCs actually work like protocols because... because
i.e. you don't need to inherit from Iterable to make your class iterable to a type checker
Yes, it could be... annexed into that site. Somewhere in the future. Or maybe it's going to be a standalone thing, just like "automate the boring stuff" isn't part of docs.python.org
||also I hate ReST ๐ฉ ||
I feel like if you first explain Protocols and how that correlates with Pythonโs duck typing, explaining collections.abc becomes a lot easier.
Just an idea.
Fair enough.
Mainly suggested it because I really liked the idea in the moment of a first-party beginner-friendly tutorial for annotations.
@rustic gull Don't ask for help with that in this server
duck typing is what i have right now
im not sure how to name the headers
||maybe we should start a patreon||
||to buy you a shift key
||
capital letters look bad
I will think about it tomorrow
whats your timezone?
horizontally or vertically
My sleep schedule is so crooked that it doesn't really matter, maybe in 14-20 hours
why isn't something like this supported by the callable type ๐ฆ
def add_partial_params(
functions: list[
Callable[ [ "GPTChatbot", "Request_data", "IndexSearchSharedState", ..., ] str, ] ,
gpt_chat_bot: "GPTChatbot",
request_data: "Request_data",
shared_state: "IndexSearchSharedState",
):
return [partial(func, gpt_chat_bot, request_data, shared_state) for func in functions]
i want all function params to have the first 3 params and other remaning parms dont matter
, Unpack[tuple[Any, ...]] maybe?
(Also functions don't need to be a list in your example, just iterable)
typing.Concatenate might be helpful here as well.
@restive rapids I added a short section on type inference and added the page to the index
i think you should move the empty list to it
Whenever you have an empty collection assigned to a variable, you need to give it an annotation:
names: list[str] = []
this one
and maybe should make hyperlinks for all things like ``x``? or thats for later
wdym
For example, json.loads() and pickle.loads() both return Any.
something like that
maybe there's a mkdocs thingy for that instead of manually writing hyperlinks
could add Literal and Enum to Frequently asked questions as a how to annotate that a value is one of a certain set of values or something like that (words are hard)
and Final for how to annotate a "constant" maybe
and maybe go over older articles and switch to new style annotations? atleast for unions
i guess i could do that and pr
hmh, while going over older stuff
@dataclass
class _RawMovie:
name: str
director: str
year: int
genres: Optional[list[str]] = None
genre: Optional[str] = None
@dataclass
class _RawMovies:
movies: list[_RawMovies]
this is probally a typo, right? should be list[_RawMovie] i think?
yes
alright, ill fix that in my pr i guess
will be my first not readme.md pr lmao
from typing import Union
AnyString = TypeVar("AnyString", bound=Union[str, bytes])
def triple(string: AnyString) -> AnyString:
return string * 3
unicode_scream = triple("A") + "!"
bytes_scream = triple(b"A") + b"!"
this shouldnt be a bound union, should it? should be a constraint or how its called
TypeVar("AnyString", str, bytes)
i wonder if typechecking could be ran over code in markdown
Yeah this is just completely wrong
https://github.com/decorator-factory/typing-tips/pull/6 i dont know how pull requests work
but i think this works (besides the place where Union is mentioned outside of code, that was probally a bad idea to change it, the Sometimes inheritance works better, sometimes you're better off with a (`A | B`) (`Union`). one.. yeah, thats aids)
might want to iterate over that too, change noreturn to never, and.. maybe, maybe change optional to | None, but i myself am not sure what to use, optional kinda reads better, but is sometimes confused that it means something magical and not just | None, which is more explicit
probally too early to accept the pr, i'll go change that and use Never instead of NoReturn
oss is hard
hmh, well, the NoReturn doc in tips uses screenshots and the theming will be different if i screenshot from my vscode.. urgh, sucks to suck, ill guess ill only change some of them, or just not care about it for now. thats probally better
ill go over other stuff for now and maybe fix it too
Maybe I should publish my theme ๐
im good with my one
ok i think besides that union change its fine
if this one gets uh. merged? idk, would i delete it? if i want to commit later would i make another branch? i never really committed
I left a small comment
When I merge the changes you can "sync" your fork and then make a new branch
or you can just commit to the master of your branch and make a PR from there
did you? i cant find maybe im blind
maybe replace optional everywhere? and maybe even delete the Union[T, None] == Optional[T] part then
and maybe even delete the "Optional" is not optional thing then whatsoever
Nah the optional article is still kinda relevant
because people will encounter Optional in code from time to time
yeah, you should usually not put unrelated changes in a single PR
and since people will use Optional less often they're more likely to be confused about it
how could i "fix" that now? would i remove that commit or? is that even a thing?
i've heard somewhere that just making another commit fixing the old one is better
is that preferred
git revert creates a commit that does the opposite of another commit
You can use git revert --no-commit <commit-hash> to "partially revert" the commit
so i'd still need a new commit
i guess just fixing this one by adding the deleted thing back would be better
Yes, it's usually better to add a new commit
||or well i could delete the fork and make a new one to not embarass myself with this one ๐||
however, if it's just you working on some git history, you can do various history changes
with git rebase -i and other arcane things, and then do git push --force-with-lease
well in the end that doesnt show up in the diff so i think thats good
actually let's leave the bound part alone, the examples need to be reworked
but I'll merge it
regarding this
we cook?
if i write that myself it will probally look very different from other stuff which is not nice
how to solve that
I think we should first make some kind of plan, or something like that
well a good first example (for Literal / Enum) is open()
it does accept a string mode but not all strings, only some specific ones
I mean a more high-level plan, for the whole website
Well, I'd hope that the stuff under the "Start here" section will serve as a thing you can read in order
hmh
perhaps there will need to be a lot of
moving around of current stuff then
and some stuff cant really be in order, you dont need everything right away, some is fairly specific - like the anti patterns one
i am constantly getting an error in a function from mypy, peelpreter/evaluator/mbuiltins.py:80: error: Incompatible types in assignment (expression has type "str", variable has type "list[Object]") [assignment],
the function is this,
def m_tail(fname: str, args: list[obj.Object]) -> obj.Object:
if len(args) != 1:
return arg_error(fname, 1, len(args), "tail")
if args[0].type == obj.OBJ_ARRAY:
assert isinstance(args[0], obj.Array)
sequence = args[0].elements
_, *tail = sequence
return obj.Array(tail)
elif args[0].type() == obj.OBJ_STRING:
assert isinstance(args[0], obj.String)
string = args[0].value
tail = string[1:] #This is the line mypy is going crazy
return obj.String(tail)
else:
return obj.Error(error.UnsupportedType(fname, args[0], "tail", (-1, -1)))
tail is literally undefined, i dont get how its getting that tail is list[Object]
mypy doesn't allow variables with different types to have the same name in the same scope
that's just kinda how mypy works
even if they are seperated by if and elif?
yep
like they are never gonna conflict since only one can be executed at a time
yep
def foo() -> bool:
raise NotImplementedError
def bar() -> None:
if foo():
hmm = 1
print(hmm + 2)
else:
hmm = "yes"
#^ error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment]
print(hmm + "!")
#^ error: Unsupported operand types for + ("int" and "str") [operator]
It's mildly annoying and it's not likely to be changed, but you could open an issue/look for existing issues
oh well
a user suggested me to type hint the var on the second mention/elif, gonna try that now
mypy will complain about that since you're "redefining" hmm
I think the simplest workaround is to rename the second variable, like rest or tail2 ||๐||
yeah
in your case I'd just do return obj.String(args[0].value[1:])
pylance wouldn't complain about this ^ right?
<?php
/**
* @template TX of FooInterface
* @template TY of FooInterface
* @phpstan-param ?TX $x
* @phpstan-param ?TY $y
* @phpstan-return ?(TX|TY)
*/
function fooInterfaceSwitched(
?FooInterface $x, ?FooInterface $y, bool $switch
): ?FooInterface
{
return $switch ? $x : $y;
}
function fooConcreteSwitched(
?FooConcrete $x, ?FooConcrete $y, bool $switch
): ?FooConcrete
{
return fooInterfaceSwitched($x, $y, $switch);
}
in php using phpstan, this (specifically, the return type of fooConcreteSwitched v.s. fooInterfaceSwitched) typechecks because phpstan can infer that, inside fooConcreteSwitched, fooInterfaceSwitched's return type can be narrowed from FooInterface to the specific FooConcrete, while of course both the code and phpstan would also support different combinations of interface implementers in other functions/uses.
i'm pretty familiar with some (casual) aspects of python's typing, but i'm not sure if something like this would typecheck correctly or what annotations would be needed. is this supported in python?
def foo_switched[T: Foo](x: T, y: T, switch: bool) -> T:
return x if switch else y
This would be more similar to the PHP equivalent:
def foo_switched[TX: Foo, TY: Foo](x: TX, y: TY, switch: bool) -> TX | TY:
return x if switch else y
``` but is not necessary, since if you call it with e.g. `FooOne` and `FooTwo`, `T` will be inferred as `FooOne | FooTwo` (maybe that's also the case in PHP?)
Before Python 3.12 you'd do ```py
F = TypeVar("TFoo", bound=Foo)
def foo_switched(x: F, y: F, switch: bool) -> F:
return x if switch else y
yeah this is pretty close to fooInterfaceSwitched, but i'm wondering if python's typing or generics features allow for stronger narrowing. my guess is "no", i know TypeGuard is pretty lame; i feel like maybe pyright would support this but as a bonus feature rather than anything in python's annotations system
what do you mean by stronger narrowing?
this would work exactly like your example
in the php code above, phpstan can verify fooConcreteSwitched's return type is correct even though it technically returns FooInterface because it knows the types of x and y and can thus narrow fooInterfaceSwitched from FooInterface to FooConcrete
oh, ok. neat!
indeed, this works too, cool. i'll also try the mypy playground
the docstring syntax in addition to PHP annotations kinda terrifies me...
mypy also used type annotations from comments in 2.x
still works in 3.x
Yeah
i prefer the comments style, actually, but might be due to limitations of my knowledge. i recall doing something weird where mypy could understand a classmethod[...] annotation from a comment, and had such a construct internally, but nothing like that exists in typing
the "reflection" you get from having the types in-interpreter is cool, but it also means we've been playing typing musical chairs since at least 3.6 as what/where the typing bits are defined has changed
classmethod is generic in typeshed, so type-checkers will understand a parameterized version of it as a type annotation or type comment, AFAIK.
One downside of the annotation is that at runtime, parameterizing classmethod like that is an error, so the annotation can only get away with it via quoting, conditional alias assignment, or something like that, whereas comments don't have to be run at runtime and can get away with whatever.
which is really something that should be fixed in all supported python versions before type comments become unsupported since that's still a goal
If the docs are any indication, it's not a difficult PR based on implementation (https://docs.python.org/3/c-api/typehints.html#c.Py_GenericAlias).
Haven't looked at the internals of classmethod/staticmethod/whatever else is builtin and only generic in stubs currently, so maybe it's not that simple.
maybe, but i dont use vscode so....
Why is the type DateT = TypeVar('DateT', bound=Union[datetime, date]) not being recognized by my linter?:(
show the code
@staticmethod
def first_day_of_next_month(date: DateT) -> DateT:
return date.replace(day=1) + relativedelta(months=1)
What do you mean by not being recognized?
typevar is not a typealias
I mean, for instance, this Django method uses a T but when my linter "looks" at it, it shows the type that is being passed as T
Don't know if I've made myself clear
From the Django stub:
It declares it as _T = TypeVar("_T", bound=models.Model)
And then uses it as Generic[_T]
The Position class it the specific T
I'm assuming the class where the first_day_of_next_month is is not generic
Let me try to explain better
DateT is either datetime or date
Here I'm calling a method with a date, is there a way so instead of DateT@... it just shows datetime.date or something like that?
if you do x = TemporalAdjuster.first_day_of_month(dt), what does hovering on x show?
you can make an overload
pyright lsp will show the appropriate overload in this context
I tried that, that shows the correct type
otherwise it shows the only available signature of the function
is it possible to override the types in a class depending on the value of its generic parameter?
i.e. something like:
@override
class Foo[True]:
x: int
y: int
@override
class Foo[False]:
x: str
y: str
class Foo[T: bool]:
x: int | str
y: int | str
...
You can do that with methods ```py
class Foo[T: (Literal[True], Literal[False])]:
@overload
def x(self: "Foo[Literal[True]]") -> int: ...
@overload
def y(self: "Foo[Literal[False]]") -> str: ...
why not```py
class Foo[T: str | int]:
x: T
because I have a bool flag that affects which type several attributes are
and I don't really feel like making 3 generic params
since the class can only be in one of the 2 states
I know it might be better to just do 2 classes here, but that doesn't really play well with the codebase I have, since the classes represent packets, and the system I work with need to have a single class for the packet
right now I just have them as unions, but it needs a bunch of casts when working with the values
which is annoying
like: ```py
if self.my_flag:
do_stuff(cast(int, self.x), cast(int, self.y))
the internal state does guarantee that if this flag is set, the values will be of those types, I just want to avoid the casts here
If this flag can change for a single object during runtime, then no, you cannot use a generic parameter in this way
python technically allows it, but it won't change in the way it's used
in that case you can use methods
so like, store the received data in unions and retrieve them with overloaded getters?
Well, I don't know how you're getting the int or the str
it's received over a socket from a server, this class just has the logic to read that data and represents it in python
that bool flag affects the reading logic
it's received first
this will probably be good enough
Can I overload functions by parameter type alone?
Yes
you may be able to use a union type instead to make things simpler
is there an easy way to differentiate between theoretically-infinite-length containers like dict[str, int], list[int], or tuple[int, ...] as opposed to fixed-size ones like tuple[int]? perhaps some kinda of Appendable protocol?
fair point. once it's made, it's not appendable. i meant during creation, where you can modify how many elements it initially has at will
in other words, t: tuple[int, ...] allows initial definitions of both t = (1, 2, 3) and t = (1, 2), etc
No, tuple is unique in the type system here, and only really by neccessity
I don't think we'd have tuple[int, ...] if there was a different builtin data type which was an immutable sequence for homogenous data (because people wouldnt use tuple for that then)
maybe you can use tagged unions to help with type narrowing
I have a function for which I've decided that dict and list will serve as sentinel values, and you can't convince me not to do this. Is there a better way to type hint this than Literal[dict, list]?
Literal[dict, list] is not valid
You can use type[dict] | type[list], though that will also match any subclass of dict or list
the closest you can do is type[dict] | type[list]
it has to be exactly dict or list, so I'll have to stick to the invalid approach.
You can stick to that approach, but that's just an invalid annotation. Type checkers will simply not understand it
It's like annotating something as "oo oo aa aa"
or with "use list or dict exactly" (which is also invalid but clearer)
I guess I could do type[dict] | type[list] and then change if value is dict (and etc with list) to if issubclass(value, dict)
||or you could stop using dict as a sentinel ๐ ||
new pep idea tbh
the alternative is creating an enum, and I don't want to introduce another thing that has to be imported.
is there more context to the code?
def traverse(data, keys: Sequence[int | str | Literal[dict, list]], raise_exception: bool = False, default: Any = None, allowed_exceptions: tuple[Exception, ...] = ALLOWED_EXCEPTIONS):
"""
For traversing JSON-like structures.
Equivalent to data[a][b][c][d], etc. where each a, b, c, d in `keys`. if not `raise_exception`, a missing key
(or index) will return `default`.
"""
keys_iter = iter(keys)
for k in keys_iter:
if k is list:
keys_tuple = tuple(keys_iter)
return [
traverse(elem, keys_tuple, raise_exception, default)
for elem in data
]
elif k is dict:
keys_tuple = tuple(keys_iter)
return {
k: traverse(v, keys_tuple, raise_exception, default)
for k, v in data.items()
}
try:
data = data[k]
except allowed_exceptions as e:
if raise_exception:
raise e
return default
return data
I haven't updated the docstring.
What does dict or list mean in the first argument?
And do you need to keep this for backwards compatibility?
wait, I didn't read correctly
And why do you not want to import enum?
I mean that I don't want users of the code (me) to have to import or use an enum for this
They already import your function
idc
Well, then you can use type[dict] | type[list] if you really hate importing things from the module you've already imported ๐
Also tuple[Exception, ...] should be tuple[type[Exception], ...]
Everyone (and @restive rapids), I made a new beta chapter for the type hints tutorial:
https://decorator-factory.github.io/typing-tips/main-tutorial/1-working-with-classes/
Any thoughts/suggestions/insults?
a bit of a concoction of various topics, but it is what it is
also very sad ๐ฟ
Also, does anyone know a decent way to track the view counts and how people follow the links?
the repl examples missing syntax highlighting
frozen prevents you from mutating the object after creation. maybe should be clarified? that like, its not that you cant "mutate" it, its that you cant reassign the attrs (but if its e.g. a list you can append to it)
Something that isn't... google analytics or other creepy-ish thing.
I don't think there's a language specifier for repls
not in mkdocs-material
I think that's okay, the default python repl doesn't have highlighting either
maybe just use py? should work besides the >>> ... stuff
although seems like its not ready for uh, class reprs
yeah
hmh, maybe should also leave a small note on what the _something convention means
Is there a good source that explains it?
You want "pycon" (it stands for "Python console" in this context)
overall i like it, simple, shows what problems you can have and how to solve them the practical way, and everyone loves kittens so the examples will surely make people not leave them cold and hungry
Not sure I like it over the monochrome
although regarding the "internal" __annotations__ attribute you should probally leave a note that those shouldnt be used directly for introspection and reference inspect.signature and typing.get_type_hints
The intended audience for this tutorial will probably not be using annotation metadata. This is more of a fun fact, and the rationale behind this being an error
though a footnote might be good
Could go the extra mile and link to the official best practices guide for accessing __annotations__ in the footnote as optional further reading: https://docs.python.org/3/howto/annotations.html.
Is there any reason for keeping :type x: and :rtype: in a function sphinx doc-string if there is already typing for the arguments and the return value?
It seems it's just duplicating the same information.
def add(a: int, b: int) -> int:
"""
Add two integers together.
:param a: The first integer to add.
:type a: int
:param b: The second integer to add.
:type b: int
:return: The sum of the two integers.
:rtype: int
"""
return a + b
there's no reason, no
I think it will in next update? or maybe that's just REPL in idle shell
yeah I've seen something about the new repl
something to the effect of it being based
I mean, if you read it, it must be based
??
(based ppl only read based things)
I read all sorts of crap
but it's based-crap by proxy
I will not judge you if you are into crap
https://docs.python.org/3/library/inspect.html#inspect.get_annotations
Always, always, always returns a freshly created dict.
damn they really drove the point home
!e
print((lambda:0).__code__.__doc__)
:white_check_mark: Your 3.12 eval job has completed with return code 0.
Create a code object. Not for the faint of heart.
Help on code object:
class code(object)
| code(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, qualname, firstlineno, linetable, exceptiontable, freevars=(), cellvars=(), /)
|
| Create a code object. Not for the faint of heart.
That's a fun warning, lol.
The REPL has colour on py313 (the >>> is lilac). But it doesn't have syntax highlighting like rich or mkdocs.
Tracebacks (inside and outside the REPL) have syntax highlighting everywhere on py313, however!
I c
And because it's not a REPL feature per se, you get colourised tracebacks in the REPL even if you pass PYTHON_BASIC_REPL=1 as an environment variable to get the old py312 REPL back.
Though I think there's another environment variable that disables all colour, everywhere. If you really like black and white.
class A:
def b(self, data: dict) -> dict:
...
How should I typehint A.b as Callable?
Callable[(A, dict), dict]?
Callable[[A, dict], dict]
why though
well, I want to store a method of a class, without initialize the class
dict[str, list[tuple[Route, Callable[[Route, type_Data], type_Data]]]]
you don't need to typehint it or I don't understand
type_Data ๐ค
that list with 2-tuples is sus. why not make a Route hashable and put in a dict?
Because a route can point to multiple string
And a string can point to multiple route
A typealias of a stricter dictionary
a TypedDict?
why did you name it like that
Well technically, it just a typehint as a json compatible dictionary
Without any conversion
ok, should be PascalCase
I love Concatenate
from collections.abc import Callable
from typing import Concatenate
def foo(x: list[float], y, z):
print(x)
MY_ALIAS = Callable[Concatenate[list[float], ...], None]
def call(f: MY_ALIAS):
pass
call(foo)
Is this an idiomatic way to express this though? Or does it do something weird (I want any argument after the first to be of type Any)
Why do you want it to take kw/var args here? You know the types for sure
^ you probably should do Callable[Concatenate[list[float], P], None] instead.
You should also add ```py
def call(f: MY_ALIAS, *args: P.args, **kwargs: P.kwargs) -> None: ...
What is the best way to type hint "you can pass either a singular int or a sequence of int, and you will get back either a float or a sequence or float depending on the multiplicity of the input"?
Using TypeVars?
use overloads
How so?
import typing as t
@t.overload
def uwu(x: int) -> float:
...
@t.overload
def uwu(x: t.Sequence[int]) -> t.Sequence[float]:
...
def uwu(x: t.Union[int, t.Sequence[int]]) -> t.Union[float, t.Sequence[float]]:
# actual implementation here
Or you can choose to only accept a sequence, and have the user call uwu([42]) instead of uwu(42)
!pep 747
hey all! is there a way for me to turn this typescript expression into its python equivalent?
enum Foo {
A,
B,
};
type Bar = { a: string; b: number; } & (
| { tag: Foo.A, value: { a: number, b: number } }
| { tag: Foo.B, value: {} }
| { tag: Exclude<string, keyof Foo>, value: { key: string, value: string }[] }
);
in essence Bar is a tagged union with some common parameters. there should be a few well-known types with type-safe representations of value, and a fallback with a generic value type
from dataclasses import dataclass
@dataclass(frozen=True)
class Common:
a: str
b: int
@dataclass(frozen=True)
class CaseA(Common):
value: Something
@dataclass(frozen=True)
class CaseB(Common):
value: SomethingElse
@dataclass(frozen=True)
class CaseC(Common):
value: None = None
hmm, but then i would have to manually cast into the given type, right? the tag is present as a value in the data i receive from an external service
You can use TypedDict if you want to trust the data with no validation/parsing
But I'm not sure TypedDict will work with the same quality of type inference as records in TypeScript
i suppose i'd still want to do validation, but i'd need to write custom deserialization to turn it into those classes, right?
You can use packages like pydantic, cattrs, adaptix to turn JSON data into "typed" classes. They're similar to zod in TypeScript
i see, do you know if they have a feature to turn my tagged data into those classes automatically? that was my main question
Not sure, you'll have to experiment with https://adaptix.readthedocs.io/en/latest/loading-and-dumping/specific-types-behavior.html#union
thank you :)
Isn't this only type checkable if Iterable requires a certain signature for __init__?
def is_iterable(obj) -> TypeGuard[Iterable[Any]]:
try:
iter(obj)
except TypeError:
return False
else:
return True
Let's say I have a TypedDict called MyDict with key1 and key2. Is there a way to indicate that a variable key can only be Literal["key1", "key2"] without defining a this new type?
I do not think there is some kind of LiteralKeys[MyDict]
But maybe there is a workaround
no
There might be some unsavory hacks depending on why you need that
Not a specific use case. It was jjst quite annoying that within my repo, I have the problem of indexing my TypedDict with a string (infered type due to function signature) when I know that this string can only take the corresponding key-str values for my TypedDict
I love python 3.10
that's weirdly specific
3.10 is the last version that had no opcode caches in bytecode
3.11+ have them, and it makes working with bytecode a lot harder
can I somehow make a test file for typing?
e.g. something like this so it would show a typing error only if test("5") will work without errors
(kind of like pytest.raises but for type checking)
def test(a: int) -> None: ...
# should fail if the expression doesn't have any typing issues
with typing.ensure_typing_issues:
test("5")
test("5") # type: ignore
``` and then enable `--warn-unused-ignores` in mypy (or the equivalent in pyright)
nice, thank you!
# pyright: reportUnnecessaryTypeIgnoreComment=error
def test(a: int) -> None: ...
test("5") # type: ignore [reportArgumentType]
def test1(a: str) -> None: ...
# Unnecessary "# type: ignore" comment
test1("5") # type: ignore [reportArgumentType]
You may also want to use typing.assert_type().
X-Posting here in case anyone is a typing expert with sqlalchemy.
but assert_type doesn't help in defining the cases that should fail, only cases that should work
well python doesn't have any concept of a Not type
hey yall, having an issue with dynamic classes and type hinting that I can't seem to figure out. Instead of typing out everything I'll just link to my post in the python help channel #1253389046030401626 message Figured people in this channel might have some idea
So nothing like Sequence[str] & ~str
lol. Good luck with Sequence[str] & ~str This one's only a problem because of a type ignore in the typeshed and str subclassing Sequence when it doesn't conform to Sequence
Wait, why doesn't it conform to sequence
Oh, __reversed__
wait, sequences aren't required to have that
Sequence takes object for contains, str only takes str
ohh
So str isn't a sequence all along ๐คฏ
So why is it forced to be a Sequence?
causing so much anguish to people expecting a Sequence[str] where a string is probably a mistake
You also cant just use the thing in useful_types for this, because str -> Sequnce[str] ->their thing
Which avoids it being detectable due to the original lie
Ultimately, lying about a type creates unsolvable siituations since there's no "total program wide consistent use" check requirement
int.hex my beloved
It seems more fundamental than that. str is documented as a "text sequence" in the main Python docs (link), and it's mentioned that it implements "all the common sequence operations"; I do think there's value in teaching it that way. The runtime registration of str as a virtual subclass of Sequence kinda mirrors that as well.
Basically, imo, this doesn't seem like typeshed's fault; it's just respecting a (seemingly) longstanding part of runtime as best as it can.
lying about the type isnt useful to people who care about the type.
teaching "str is implemented so that it behaves very similar to collections" doesnt preclude the type being accurate
This is very much a "who is typing for?" moment
True. I guess I'm just trying to come up with reasons that make sense for why it's the way it is, because I doubt it was a simple choice to lie for no benefits. At the very least, I'd guess that this virtual subclassing was happening before PEP 484 was accepted.
Do you think this line applies to int/float as well?
Oh, so this has been a thing since pre-2.7, if you count basestring being a virtual subclass as well. Damn. https://github.com/python/cpython/commit/3172c5d263eeffff1e89d03d79be3ccc1d60fbde#diff-163c1525f292b112e15d4d78952fb8bc6a4b80e6ec8d608eea37edad2cfed28e
Feels like it would be easier to bend *str.whatever to accept object at this point and have __reversed__ for consistency /hj
I think that in the case of a gradual type system which isn't enforced at runtime, it is better to leave things untyped or partially typed with accurate structural information than it is to lie about the type
Its also hard to fix retroactively, as some of this predates the type system
its interesting you bring up float/int though. For the same reasons the numbers module is unsafe to use as typehints, so are some of the collections in collections.abc
I bring this up because for some reason that design choice seems particularly strange to me. I might be hyper-focused on it a bit (and some people love it)
It costs quite a bit in an on-demand computing situation to have ints where floats were expected. It has the appearance of making sense right up until the difference matters
I don't think I quite understand you
native code where float math is cheaper than arbitrary precision integer math, dispatching on the types provided
ah
well it has to do type checking anyway, right? ๐ค
though converting an int to a float might eat up a lot
Yes, but the native code cant know the type it got was "unepexpected" in a situation where the native code is translating code from math majors, not CS majors. $work added some temp workarounds for that, but the longterm was dropping python for this usecase ๐ฆ
You cant typecheck numerics from python being "actually a float", theres no way to exclude ints statically
Anyhow, i got asked by a few people "how do you fix it" and ill write up my thoughts more constructively at a later date
The lying about types thing though just breaks the ability to do logic, from false premises you can claim anything
Should there be an asyncio.gather2 that actually returns a tuple?
Well, not sure if that's super relevant since we have task groups nowadays
but in general, when you have an older API that's impossible to type, and it would cost time/memory to do some conversion
I mean, my main gripe was not with accepting an int or a float in many cases, that can already be done with float | int ||(maybe math majors don't run type checkers?)||. It was with the virtual subtyping that doesn't match the runtime
probably not, you can type it accurately enough for the usecases that dont fit into task groups (rare, but exist) with typevar tuples
asyncio.gather returns a list at runtime, that's what I meant
๐ค yeah, but what's the issue with that?
In the typeshed it's typed as returning a tuple when you're calling it with a fixed number of arguments
all the uses for gather require you keep a reference to original coro/tasks anyhow
gather takes varargs. Presumably you do have an actual source for what goes into those. Theres not actually a reasonable case where you would care about the task results, and have heterogenous task result values, and didnt construct tasks ahead of time (now that task wrapping automatically isnt a thing)
List[Task[Any]] would be a perfectly acceptable return type too
From the asyncio.gather docs;
If any awaitable in aws is a coroutine, it is automatically scheduled as a Task.
async def main():
# Schedule three calls *concurrently*:
L = await asyncio.gather(
factorial("A", 2),
factorial("B", 3),
factorial("C", 4),
)
print(L)
maybe I'm misunderstanding what you mean
Deprecated since version 3.10: Deprecation warning is emitted if no positional arguments are provided or not all positional arguments are Future-like objects and there is no running event loop.
so, list[Future[Any]]
could do list[Future[Union[*Ts]]] if union is undprecated and gains unpack support
oh
Has typing-extensions ever had a breaking change?
yes. mostly unintentionally but the 4.0 release removed some stuff
That's good. It means my response on this discussion isn't wrong.
https://github.com/tiangolo/fastapi/discussions/11746#discussioncomment-9840852
Yeah I try to never make any more breaking changes in typing-extensions because users' lives become very difficult if libraries start upper-bounding typing-extensions
So I don't want to give people reasons to use upper bounds
what's the simplest way to narrow down the type of the list elements?
def ret() -> list[int | None]: return []
a = ret() # list[int | None]
# something like assert all(isinstance(i, int) for i in a)
...
reveal_type(a) # list[int]
It is impossible
You can make a copy of the list with only ints in it
The reason it's impossible is (by the means of an example):
things: list[int | None] = [1, 2, 3]
def ret() -> list[int | None]:
return things
a = ret() # list[int | None]
# do some kind of checking, now a: list[int]
things.append(None) # completely valid action
# but now `a` has a `None` in it. but we think it's a `list[int]`. That's very bad.
Why are you returning some Nones in that list by the way?
I meet the same case from time to time when I have something like lst = [get_object_part(o) for o in elements] .
Where get_object_part returns either some type A or None. Typically, in 95% cases it's always just list[A] but I wanted to guard myself somehow from those Nones and to make sure they won't be just skipped if I do lst = [o_:=get_object_part(o) for o in elements if o_]
How can I copy the type hints of a Callable? For instance: Callable[..., T] , what shuold go in ...?
def not_null[T](item: T | None) -> T:
if item is None:
raise ValueError("Unexpected None value")
return item
...
lst = [not_null(get_object_part(o)) for o in elements]
``` or something like ```py
lst = filter(None, (get_object_part(o) for o in elements))
``` if the object is otherwise always truthy
Can you show an example?
Yeah, I was just wondering if there is some general tool already available from typing / typing_extensions that I'm missing, I was imagining for it to work something like a = [i for i in a if assert_(i, int)] or a = [assert_(i, int) for i in a]
typing provides very little in terms of runtime checking (because it's for static typing)
I think filter(None is kinda useful
!d filter
filter(function, iterable)```
Construct an iterator from those elements of *iterable* for which *function* is true. *iterable* may be either a sequence, a container which supports iteration, or an iterator. If *function* is `None`, the identity function is assumed, that is, all elements of *iterable* that are false are removed.
Note that `filter(function, iterable)` is equivalent to the generator expression `(item for item in iterable if function(item))` if function is not `None` and `(item for item in iterable if item)` if function is `None`.
See [`itertools.filterfalse()`](https://docs.python.org/3/library/itertools.html#itertools.filterfalse) for the complementary function that returns elements of *iterable* for which *function* is false.
Though if you have ints in the actual list, it's no good
but filter has the same problem as adding if i to the comprehension - some None values might slip in
no, I mean that they will be skipped
Ah, so you want to raise an error if you get a None?
Maybe it's something you should do in the get_object_part function then?
well, this is the problem ๐
this function was added a while ago and codebase sometimes relying on it able to return None
I'm thinking that maybe I should just add get_object_part_safe
that will raise an exception
yeah that's definitely an option
or get_object_part(obj, safe=True)
that will create some nasty overloads
Yeah, they make the function more complicated. And type checkers can't verify that your overloads are correct
Also, if you make a new function, it's easier to deprecate the old function and slowly replace it, if you think raising an exception is a better default
If it's a local comprehension then #type-hinting message clearly doesn't apply. So you could just do ```py
if None in lst:
raise ValueError(...)
lst_safe = typing.cast(list[Obj], lst)
As always, if it's just one time, you can do anything you want ๐ but you need to assess whether it will happen a second time
(or just note it somewhere)
it's interesting option, I haven't thought about it
Sure!
def sequenceable(target: str) -> Callable[[Callable[..., T]], Callable[..., Union[T, Sequence[T]]]]:
"""
This decorator processes a sequence of values passed as an argument to a function.
The function is called for each value in the sequence, and the result is stored in the same position in the sequence.
"""
The problem with all of the "non-invasive" options so far is that you don't know which o caused an object part to not be found. It's going to make debugging a misery @tacit sparrow
!d typing.ParamSpec
class typing.ParamSpec(name, *, bound=None, covariant=False, contravariant=False)```
Parameter specification variable. A specialized version of [type variables](https://docs.python.org/3/library/typing.html#typevar).
In [type parameter lists](https://docs.python.org/3/reference/compound_stmts.html#type-params), parameter specifications can be declared with two asterisks (`**`):
```py
type IntFunc[**P] = Callable[P, int]
``` For compatibility with Python 3.11 and earlier, `ParamSpec` objects can also be created as follows:
```py
P = ParamSpec('P')
``` Parameter specification variables exist primarily for the benefit of static type checkers. They are used to forward the parameter types of one callable to another callable โ a pattern commonly found in higher order functions and decorators. They are only valid when used in `Concatenate`, or as the first argument to `Callable`, or as parameters for user-defined Generics. See [`Generic`](https://docs.python.org/3/library/typing.html#typing.Generic) for more information on generic types.
How would you call this function? Can you show a more concrete example?
this will have a problem with strings wont it
What do you mean by more concrete? But here's an example call
@sequenceable(target='date')
def last_day_of_next_week(date: DateT) -> DateT:
What type should the resulting function have?
(date: DateT) -> Sequence[DateT]:
So maybe ```py
parts: list[Part] = []
for o in elements:
if part := get_object_part(o):
parts.append(part)
else:
raise RuntimeError(f"Part not found for {o!r}")
not -> DateT | Sequence[DateT]?
I don't see how that matches the docstring. Shouldn't the resulting function accept a sequence?
Can you show the implementation?