#type-hinting

1 messages · Page 45 of 1

feral wharf
#

that's neat

#

t's supposed to always return a sequence

#

but since users can pass single items too, pages[n] can be one of those too

#

I assume that's just not possible in python

trim tangle
#
if isinstance(page, Sequence) and not isinstance(page, str):
    ...
#

and you'll probably need a # pyright: ignore

#

That's because, theoretically, an object could be both e.g. a sequence and a discord.File (using multiple inheritance)

#

you gotta be real careful here because if e.g. some of those is a NamedTuple, you are in trouble (because it's also a sequence)

#

A more robust check would be something like... py if not isinstance(page, (str, discord.ui.WhateverCommonBaseClassThoseHave, discord.File, ...etc)): ... which you could extract into a TypeIs function

#

This is definitely an issue with "untagged unions". It can be difficult to figure out which of the cases the object is (and what if it matches both?). Programming languages with (safe) unions typically have an explicit "tag" or "case" for each of the options ```rs
pub enum Page<'a>{
Text(&'a str),
File(discord::File<'a>),
Component(&'a discord::UiThing),
Mystery(HashMap<&'a str, &'a dyn Any>),
Nested(&'a[Page<'a>]),
}

#

and you could do this in Python in various ways, like ```py
type Page = tuple[Literal["text"], str]
| tuple[Literal["file"], discord.File]
| tuple[Literal["nested"], Sequence[Page]]
| tuple[Literal["mystery"], dict[str, Any]]

feral wharf
trim tangle
#

It's new in python 3.13, so if you're using 3.12 then you need to import it from typing_extensions

stray summit
trim tangle
#

yeah, you should not do this

#

that is not idiomatic python

#

(though types have arguably changed what is considered idiomatic python)

fiery canyon
#

I'm using Py 3.10.6. Is there any reason to use the TypeAlias? Everything seems to be working just fine without it, my typechecker doesn't care.

#

And another question. Is it fine that I use collections.abc for these 2 instead of typing? They work just fine for type hinting from what I can observe. Notice the Py version I use in the message above

stable fjord
#

TypeAlias is good to indicate intent, and may be necessary in some complicated cases iirc
importing from collections.abc is the preferred way of doing it if supported by the python version you use

trim tangle
fiery canyon
#

Now I'm left with this lol

#

But I'm pretty sure that for 3.10 there's no alternative

rare scarab
#

Try this. ```py
from future import annotations
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import TypeVar

trim tangle
#

!?!?!

oblique urchin
trim tangle
#

I guess you could do ```py
if TYPE_CHECKING:
T = TypeVar("T")
else:
class T: pass

#

if you want to avoid importing typing at all costs

rare scarab
#

Just noticed I imported TypeVar. I meant to import TypeAlias

#

damn muscle memory.

soft matrix
trim tangle
#

every good movie plot starts with

I wasn't aware there were discrepancies between type checkers about this

trim tangle
#

actually, this isn't quite right. wait a moment

#

Oh right I figured out what's wrong

#

this: ```py
class PairIdentity(Protocol):
def call[A, B](self, arg: tuple[A, B], /) -> tuple[A, B]:
...

def my_identity[X, Y](arg: tuple[Y, X], /) -> tuple[Y, X]:
return arg

def call_pair_identity(fn: PairIdentity & FunctionType) -> None:
f = fn[int, str]

call_pair_identity(my_identity)

#

it's passing the arguments that are in the protocol's order but they're in the wrong order for the runtime function

#

Maybe a sensible behaviour is to just return the function itself when it gets subscripted? (regardless of how many arguments are passed or whether it's generic at runtime at all)

trim tangle
#

I finally started working on my generics tutorial. Thoughts on this?
https://decorator-factory.github.io/typing-tips/tutorial/3-generic-functions/
(and any opinions on the TODOs?)
I'm planning to have these articles:

  • generic functions
  • generic classes (the basics, mostly explaining syntactic features)
  • the difficult article where I explain variance (current plan: explain the concepts and the reasoning, and then drop the definitions int he very end; with the goal of building a solid mental model first)
  • callable protocols and protocols describing
  • typing decorators (also introduces ParamSpec and TypeVarTuple)
  • leftover features (typevars with constraints, LiteralString bound, differences between type checkers etc,)
restive rapids
# trim tangle I finally started working on my generics tutorial. Thoughts on this? <https://de...

overall i like it, i think that the most important part is the start where you show that some functions can work with different types and "have types of values linked together in some way"

there are a couple places where i might change something:

Type variables support setting a bound, requiring that the solution to the type variable must fit the bound.
i think this is a bit ambiguous if you dont already know what it means, maybe change that the solution type must be a subtype of the bound type

and, as a purist, i would add hashable bounds to places where the typevar is used as a dict key type

# add T: Hashable
def count[T](items: Iterable[T]) -> dict[T, int]:
    counter: dict[T, int] = {}
    for item in items:
        counter[item] = counter.get(item, 0) + 1
    return counter

# add K: Hashable
# maybe change keys to Container[K], we dont use len(keys) or iterate over it
def pick[K, V](m: Mapping[K, V], keys: Collection[K]) -> dict[K, V]:
    return {k: v for k, v in m.items() if k in keys}

(im not sure why there's no bound on dict's key type to be Hashable)

and,

One common application of generic functions is processing collections of unknown types. When doing so, you should almost always accept a general type like Iterable or Iterator or Mapping instead of concrete classes like list or dict.
this doesnt explain why you should do that, maybe it could link to https://decorator-factory.github.io/typing-tips/tutorial/2-using-protocols/ and have that post explain why 🤷

trim tangle
# restive rapids overall i like it, i think that the most important part is the start where you s...

i think this is a bit ambiguous if you dont already know what it means
Yes, that's definitely hand waving a bit. I haven't introduced the terms "subtype" or "assignable" yet so I didn't want to use them. I was thinking of doing that in the variance article, because that's when it matters the most. But maybe it could be good to explain that in the context of e.g.: why I can't do x: int = foo where foo: int | str | Banana

#

this doesnt explain why you should do that
Good point. I originally wanted to postpone that because of variance, but it's probably better to substantiate it in some way at least (like: "that makes the function more flexible and signals that you're note mutating the arguments" or something)

#

Container is kinda weird. I assumed that it also includes all iterables, because you can do x in iterable. But apparently not

#

in that case maybe it is appropriate

fiery canyon
#

Ah I just saw what you said below

#

Nah it doesn't matter that much, I'm fine as long as my code works for newer versions of py (I care less about older versions)

#

And it's not like typing takes much memory space either

fiery seal
marsh gorge
#

i realized i have an issue with my parser library input protocol. i have a __getitem__ that returns Self, but bytes.__get__item may or may not return an int or a bytes

#

where str.__getitem__ always returns str

trim tangle
#

indeed

marsh gorge
#

python not having a char type makes this kind of funky

#

i thought i could make the Input protocol generic and have an overload on __getitem__ but type parameters cant have generic bounds

rare scarab
#

add an override for slice

marsh gorge
#

or else what do i return from the overloaded method

trim tangle
marsh gorge
#

hm that might work

#

ill try that

trim tangle
# marsh gorge python not having a char type makes this kind of funky

here's some unicode sadness btw

>>> s = "apple\ud8ffbanana"
>>> s
'apple\ud8ffbanana'
>>> print(s)
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    print(s)
    ~~~~~^^^
UnicodeEncodeError: 'utf-8' codec can't encode character '\ud8ff' in position 5: surrogates not allowed
>>> 
marsh gorge
#

now i have an interesting issue being generic over Sequence[I], when i feed it a string, the return type of the parser is Sequence[str] and not str

#

which is expected i suppose

#

how do i flatten it back into str

spiral fjord
#

"".join(seq)

restive rapids
marsh gorge
#

the parser is generic over Sequence[I] like this

class Parser[I, O](ABC):

    @abstractmethod
    def parse(self, input: Sequence[I]) -> tuple[Sequence[I], O]:
        pass```
#

so to feed it in a string it ends up with a Sequence[str]

trim tangle
#

Maybe you could do ```py
class ParserI, O:
@abstractmethod
def parse(self, input: I) -> tuple[I, O]:
pass

marsh gorge
#

i need something slicable i think at least

trim tangle
#

where?

marsh gorge
#

here is an example of a core parser

@dataclass
class Take[I](Parser[I, Sequence[I]]):
    _n: int

    def parse(self, input: Sequence[I]) -> tuple[Sequence[I], Sequence[I]]:
        return input[self._n :], input[: self._n]```
#

or ```python
@dataclass
class TakeWhile[I, O](Parser[I, Sequence[I]]):
_parser: Parser[I, O] | Callable[[I], bool]

def parse(self, input: Sequence[I]) -> tuple[Sequence[I], Sequence[I]]:
    match self._parser:
        case f if callable(f):
            i = 0

            while f(input[i]):
                i += 1

            return input[i:], input[:i]

        case Parser():
            start = len(input)
            rest = input

            while (rest, _ := self._parser.parse(rest)):
                pass

            d = start - len(rest)

            return input[d:], input[:d]```
#

it works by returning rest, consumed

trim tangle
#
@dataclass
class Take[I](Parser[Sequence[I], Sequence[I]]):
    _n: int

    def parse(self, input: Sequence[I]) -> tuple[Sequence[I], Sequence[I]]:
        return input[self._n :], input[: self._n]
marsh gorge
#

wont that run into the same issue if i use that parser on a str?

trim tangle
#

Yes, if you use Take[str], it's going to turn str into a generic Sequence[str]

restive rapids
#

(which is not wrong, a str is a Sequence[str], unfortunately, as there is no character type)

marsh gorge
#

it would be nice if mypy allowed intermixing str with Seq[str] in some cases

restive rapids
#

[E, I: Sequence[E]](I) -> I could solve this if bounds could contain typevars, i think

marsh gorge
#

that was my original design sorta

trim tangle
#

What about this ```py
class Slicable(Protocol):
def getitem(self, arg: slice, /) -> Self:
...

@dataclass
class Take[X: Slicable](Parser[X, X]):
_n: int

def parse(self, input: X) -> tuple[X, X]:
    return input[self._n :], input[: self._n]
#

hmmmm I think this won't work

#

beacuse str has __getitem__(self, arg: slice, /) -> str, not __getitem__(self, arg: slice, /) -> Self

#

yeah

marsh gorge
#

also that would make parsing bytes weird i think because bytes[0] returns int

trim tangle
#

but bytes[0:] is bytes

#

Do you actually need to support parsing both str and bytes?

marsh gorge
#

i would like to be able to but i guess its not super critical

trim tangle
#

maybe you're procrastinating on actually parsing some text 🙂

marsh gorge
#

haha

restive rapids
trim tangle
#

hmmmm

trim tangle
# restive rapids i dont think that makes it "not work", it typechecks on mypy --strict and pyrigh...
from typing import Protocol, Self, Literal
from dataclasses import dataclass

class Slicable(Protocol):
    def __getitem__(self, arg: slice, /) -> Self:
        ...

@dataclass
class Take[X: Slicable]:
    _n: int

    def parse(self, input: X) -> tuple[X, X]:
        return input[self._n :], input[: self._n]

t = Take[Literal["banana"]](2)
banana1, banana2 = t.parse("banana")
``` this passes pyright and pyright thinks that `banana1` and `banana2` are both `Literal["banana"]` 😭
#

this type system is actually cooked

restive rapids
#

literal is cooked
its a type that doesnt have to do with types

trim tangle
#

mypy has the same bug for some reason

oblique urchin
#

I feel using Self in a protocol is the dubious thing here

brazen jolt
oblique urchin
#

it shouldn't pass for (non-Literal) str either, str.__getitem__ returns str not Self

#

so this would also be wrong for a subclass of str

trim tangle
brazen jolt
#
class Slicable(Protocol):
    def __getitem__(self: AnyStr, arg: slice, /) -> AnyStr: ...
#

this one might be a better choice

mossy rain
brazen jolt
#

if the author just wants str | bytes

trim tangle
#

ah hmm

#
class Slicable(Protocol):
    def __getitem__[S: (str, bytes)](self: S, arg: slice, /) -> S: ...
#

rare constrained typevar W

#

(iirc AnyStr is deprecated)

brazen jolt
#

yep

brazen jolt
#

didn't know that

trim tangle
#

!d typing.AnyStr

rough sluiceBOT
#

typing.AnyStr```
A [constrained type variable](https://docs.python.org/3/library/typing.html#typing-constrained-typevar).

Definition...
trim tangle
#

one of the definitions of all time

mossy rain
brazen jolt
#

but yeah, seems that it is, since 3.13

#

interesting

#

makes sense though

trim tangle
#

Do you think I should open a bug in both mypy and pyright?

mossy rain
rough sluiceBOT
#

Lib/typing.py line 2766

AnyStr = TypeVar('AnyStr', bytes, str)```
brazen jolt
trim tangle
#

Yeah, unfortunately deprecations don't always manifest at runtime...

brazen jolt
#

runtime warnings will come at 3.16+

mossy rain
trim tangle
#

so I'll try mypy first

oblique urchin
#

But agree the more direct bug here seems to be that str shouldn't obey that protocol

trim tangle
#
class UserList:
    def __getitem__(self, slc: slice, /) -> Self:
        return type(self).from_iterable(self.data[slc])
plain dock
# brazen jolt

so not even python -W default $file or 3.14-targeted ruff even suggest something wrong about this yert

trim tangle
#

or a comment in __init__ to not change the signature pretty please

oblique urchin
#

soundness by contract

trim tangle
#

License: GNU AGPL v3 + __init__ compatibility Clause

#

I guess that technically contradicts the GPL because it limits the use of the software

trim tangle
#

I'm not sure whether I should include constrained type variables. From what I gathered type checkers have quirky thoughts on them

oblique urchin
#

they're weird but they do appear in practice

#

My sense is the concept was basically invented for the purpose of AnyStr, which was more relevant when we still had to think about Python 2

trim tangle
#

I'll probably make a separate micro-chapter on them. With re.Pattern as an example

oblique urchin
#

memoryview is also one where it makes sense, it can hold a limited set of fixed types

trim tangle
#

I think that's a decent use of the feature

mossy rain
trim tangle
#

holy hell, mypy has so many open issues

oblique urchin
#

but probably more people know about re.Pattern so it's a better example

plain dock
plain dock
trim tangle
#

pyright only has 134 open, and the oldest ones are from march 2025

#

the oldest open mypy issue is from 2013

rare scarab
#

Does pyright have stalebot?

#

IIRC, pyright also tends to close a lot of issues as not planned

brazen jolt
trim tangle
#

yeah, issues are closed as "as designed", even if it's clearly a bug that they just don't plan to fix

plain dock
trim tangle
#

Hmmm actually no, this is not right

#

Because if a type T conforms to a protocol, then a subtype U also must conform to a protocol

#

Literal["x", "xy"] doesn't conform to the protocol, therefore T2 also cannot conform to the protocol

#

so only T0 works

spare spire
#

Appropriate that fix error is demanding the fixing of errors.

soft matrix
#
   @overload
   def make[*Ts](*xs: *Ts) -> tuple[*Ts]: ...
   @overload
   def make[T](x: T) -> T: ...
   @overload
   def make(x: str, y: str) -> tuple[int, int]: ...

   reveal_type(make[int](1))             # type is int | tuple[int]```is this meant to be this or should it just be tuple[int]?
soft matrix
#

because you shouldn't be able to assign my_identity to PairIdentity if you're flipping around typevar order

trim tangle
#

🤔

#

What's unsafe about that?

#

It still describes the same set of callables

trim tangle
#

(assuming we get intersections at some point)

trim tangle
rough sluiceBOT
#

more_itertools/more.pyi line 737

def repeat_each(iterable: Iterable[_T], n: int = ...) -> Iterator[_T]: ...```
trim tangle
#

Btw, basedpyright (in the next release) now has this feature: ```py

pyright: enableBasedFeatures=true

from typing import dataclass_transform

@dataclass_transform(skip_replace=True, frozen_default=True)
def frozen[T: type](t: T) -> T:
return dataclass(frozen=True, slots=True)(t)

check that this enables covariance:

@frozen
class Box[T]:
value: T

box1: Box[str] = Box("test")
box2: Box[str | int] = box1
``` you can just disable __replace__ generation if you don't care about it

feral wharf
#

is there a guide or docs on what do to if you're using cls.__annoations__ and want to support 3.14?

trim tangle
#

(if you're already handling normal non-from __future__ import annotations annotations)

#

!e

def foo(x: CoolNumber, y: str) -> list[CoolNumber | str]:
    return [x, y]

CoolNumber = int

print(foo.__annotations__)
rough sluiceBOT
feral wharf
#

oh? I got an attributeerror on the value being a dict vs what I expected

#

have yet to check it out lol

trim tangle
#

can you show the code?

feral wharf
trim tangle
#

can you show a traceback?

#

Btw, you are not supposed to invent custom dunder names. They are reserved in Python

feral wharf
#

o didn't know that

soft matrix
trim tangle
#

Here's another example of a function that satisfies this protocol: ```py
def identity[T](arg: T, /) -> T:
return arg

soft matrix
soft matrix
soft matrix
trim tangle
#

sorry, fixed

trim tangle
soft matrix
#

i just think thats a bit silly and shouldnt be allowed

trim tangle
#

Also: type variables defined with the old-style TypeVar do not have a defined order

soft matrix
trim tangle
#

I meant that the function doesn't have type parameters at runtime

trim tangle
#

For a micro-survey:

// TypeScript
type PairIdentity = <A, B>(pair: [A, B]) => [A, B]

function identity1<X, Y>(pair: [X, Y]): [X, Y] { return pair }
function identity2<X, Y>(pair: [Y, X]): [Y, X] { return pair }
function identity3<T>(pair: T): T { return pair }
function identity4<A, B>(pair: A | B): A | B { return pair }

const _t0: PairIdentity = identity1
const _t1: PairIdentity = identity2
const _t2: PairIdentity = identity3
const _t3: PairIdentity = identity4
-- Haskell
type PairIdentity = forall a b. (a, b) -> (a, b)

identity1 :: forall a b. (a, b) -> (a, b)
identity1 (a, b) = (a, b)

identity2 :: forall a b. (b, a) -> (b, a)
identity2 (b, a) = (b, a)

identity3 :: forall t. t -> t
identity3 x = x

_ = (identity1 :: PairIdentity)
_ = (identity2 :: PairIdentity)
_ = (identity3 :: PairIdentity)
soft matrix
trim tangle
#

Here's another example: py def identity[A, B](thing: A | B) -> A | B: return thing it does fit PairIdentity, and it also has two type variables, but they're used for a completely different meaning

trim tangle
# soft matrix maybe im goofing then idk

on the other hand, Rust only allows the implementation for Apple here ```rs
trait PairIdentity {
fn foo<A, B>(&self, p: (A, B)) -> (A, B);
}

struct Apple;
impl PairIdentity for Apple {
fn foo<A, B>(&self, p: (A, B)) -> (A, B) {
p }}

struct Banana;
impl PairIdentity for Banana {
fn foo<A, B>(&self, p: (B, A)) -> (B, A) {
p }}

struct Cherry;
impl PairIdentity for Cherry {
fn foo<T>(&self, p: T) -> T {
p }}

There's some information here <https://doc.rust-lang.org/beta/error_codes/E0049.html> but it's rather limited
tight aspen
#

is there any people using really python for math?

feral wharf
# trim tangle can you show a traceback?

bit of a delay, but got it, lol

(somerandomapi.py) PS C:\Users\Sohea\OneDrive\Documents\projects\wrappers\somerandomapi.py> uv run run.py
Traceback (most recent call last):                                                                                                                                                                                                  
  File "C:\Users\Sohea\OneDrive\Documents\projects\wrappers\somerandomapi.py\run.py", line 1, in <module>
    import somerandomapi
  File "C:\Users\Sohea\OneDrive\Documents\projects\wrappers\somerandomapi.py\somerandomapi\__init__.py", line 7, in <module>
    from .clients import *
  File "C:\Users\Sohea\OneDrive\Documents\projects\wrappers\somerandomapi.py\somerandomapi\clients\__init__.py", line 10, in <module>
    from .animal import AnimalClient as AnimalClient
  File "C:\Users\Sohea\OneDrive\Documents\projects\wrappers\somerandomapi.py\somerandomapi\clients\animal.py", line 16, in <module>
    from ..models.animal_fact import AnimalImageFact, AnimalImageOrFact
  File "C:\Users\Sohea\OneDrive\Documents\projects\wrappers\somerandomapi.py\somerandomapi\models\__init__.py", line 7, in <module>
    from .animal_fact import *
  File "C:\Users\Sohea\OneDrive\Documents\projects\wrappers\somerandomapi.py\somerandomapi\models\animal_fact.py", line 9, in <module>
    class AnimalImageFact(BaseModel, frozen=True, validate_types=False):
    ...<9 lines>...
        """The animal image URL."""
  File "C:\Users\Sohea\OneDrive\Documents\projects\wrappers\somerandomapi.py\somerandomapi\models\abc.py", line 203, in __new__
    if value.init:
       ^^^^^^^^^^
AttributeError: 'dict' object has no attribute 'init'
soft matrix
#

Yeah that link really needs some work lol

#

Well just the other cases adding

feral wharf
# feral wharf bit of a delay, but got it, lol ```py (somerandomapi.py) PS C:\Users\Sohea\OneDr...
annotations: {'__attributes__': 'dict[str, Attribute]', '__init_attributes__': 'dict[str, Attribute]', '__endpoint__': 'Endpoint', '__reserved_attributes__': 'set[str]', '__possible_options__': 'dict[str, Any]', '__frozen__': 'bool', '__validate_types__': 'bool'}
key, value, _type: __init_attributes__ {} dict[str, Attribute]

looks I forgot to add __init_attributes__ to __reserved_attributes__

#

weird that it works fine pre 3.14

#

oh

#

the annotations of the metaclass BaseModelMeta weren't included before

#

and only that

feral wharf
#
if PY_314:
    import annotationlib

    annotations = annotationlib.get_annotations(self)
else:
    annotations = self.__annotations__

Seems to work

oblique urchin
indigo halo
#

With Pydantic, I have a class that has a field that is declared with an ABC as a type. Obviously, serialization is not a problem, that is where polymorphism comes to play. However, on validation, I get data, and now what? I cannot instantiate the ABC, but I also don't know what specific derived class to use. How do people solve this? I don't have a list of all available derivatives to duck-try in order, and even if, that would be a horrible way. Should I serialise the qualified name of the class that dumped the data, and then try to use importlib to instantiate it?

feral wharf
trim tangle
#

Directly using the class path&name is not great. It's bug-prone, tightly coupling the data format to your current implementation, and it can present security risks. So it's better to have a more explicit mapping. For example, you could add a concrete class to the list in __init_subclass__, and use some sort of class variable as the discriminator. Or make a registration function that a plugin can call

marsh gorge
#

are there any plans or PEPs currently in progress to allow parameterized bounds? aka class Foo[T: Bound[U]]

trim tangle
#

Here's an example of a security problem: you have some other part of the system save cached user-provided types to a directory named foo_cache, and a malicious user with the ID bob123 uploaded a malicious file. That file got saved as foo_cache/bob123_a0befae698c3fdafcbf.py, and bob got to know this somehow. Now, if you're not careful, bob can provide the import path as foo_cache.bob123_a0befae698c3fdafcbf.MyClass and it's going to execute the malicious code

#

Or it could be more mundane such as providing builtins.int for the path and the class ending up not matching the ABC

indigo halo
#

yes, I can see the issue for sure.

merry falcon
#

Hello, I'm trying to create a metaclass for classes with memoized instances, but I'm struggling to type hint the INSTANCES dictionary in the __init__ method below (I'd like the value type to be cls). I've tried using type variables to make __init__ generic but still cannot get this working. Any advice?

    __KEY_PARAMETER: str

    @classmethod
    def __prepare__(
        mcls,
        name: str,
        bases: tuple[type, ...],
        /,
        *,
        key_parameter: str,
        **kargs: Any,
    ) -> MutableMapping[str, object]:
        namespace: MutableMapping[str, Any] = super().__prepare__(
            name, bases, **kargs
        )
        namespace["__KEY_PARAMETER"] = key_parameter
        return namespace

    def __init__(cls, *pargs: Any, **kargs: Any) -> None:
        super().__init__(*pargs, **kargs)
        # Ideally change Any in line below to cls
        cls.__INSTANCES: WeakValueDictionary[Hashable, Any] = (
            WeakValueDictionary()
        )

    def __call__(cls, *pargs: Any, **kargs: Any) -> Any:
        if (key := kargs[cls.__KEY_PARAMETER]) in cls.__INSTANCES:
            return cls.__INSTANCES[key]
        else:
            return super().__call__(*pargs, **kargs)
indigo halo
#

If I change the type of a field with a field_serializer that has an altering return type, e.g.

from typing import Any

import pydantic


class MyCls(pydantic.BaseModel):
    number: int

    @pydantic.field_serializer("number", mode="plain")
    def _num_to_str(self, value: Any) -> str:
        return str(value)


print(MyCls.model_json_schema()["properties"]["number"]["type"])

then it would be reasonable for me to expect that the MyCls.model_json_schema() return should reflect this, right? What could be the reason that this isn't working? The above code prints "integer", when I am expecting "string".

tame flame
#

Folks, is there any way to typehint this? I don't want to write a Protocol

def get_run_command_func(service) -> Callable[[str, "maybe a bool idk?"], str]:
    def run_command(command: str, *, raise_on_fail: bool = False) -> str:
        success, result = service.run(command)
        if raise_on_fail and not success:
            raise Exception(result)
        return result

    return run_command
indigo halo
trim tangle
tame flame
#

It's a simplified version of a pytest fixture

trim tangle
tame flame
#

PyCharm does

trim tangle
#

ah...

tame flame
#

Doesn't MyPy? 🤔

trim tangle
#

well, pytest fixtures require special handling obviously

#

not sure if there's a mypy plugin

trim tangle
#

pyright does

tame flame
#

I get "unexpected argument" errors when using the kwarg is the issue I'm having 😄

#

Wait you mean remove the argument type?

trim tangle
#

then it seems like you'll have to make a protocol

trim tangle
# tame flame Wait you mean remove the argument type?

I meant this ```py
def get_run_command_func(service):
def run_command(command: str, *, raise_on_fail: bool = False) -> str:
success, result = service.run(command)
if raise_on_fail and not success:
raise Exception(result)
return result

return run_command
tame flame
#

Ah, right, lemme check

#

Well, it stops complaining

#

This is what it infers

#

You know what, that's good enough, these are only tests anyways 😄

#

Thanks for the help!

trim tangle
#

🤔 what is that Any?

tame flame
#

The asterisk, I guess

trim tangle
#

pycharm was vibe coded before we had LLMs

tame flame
#

ikr

stiff acorn
#

is there a way to type a dict subclass similar to a typed dict (eg some keys and their types)

#

(yes, I should just use dataclass and other fancier things. I would if I could)

lament scaffold
#

maybe this:

if TYPE_CHECKING:
  keys: Literal["key1", "Key2"] | your_other_types
  ...
stiff acorn
#

I meant the per key kind

#
# real runtime subclass of dict that guarantees some keys and their types
class Foo(dict):
     key: Type
     key2: NotRequired[Type]
lament scaffold
#
from collections.abc import ItemsView
from typing import TYPE_CHECKING, Literal

class Test(dict):
    if TYPE_CHECKING:
        items: ItemsView[Literal["one", "two"], int]

maybe something along these lines?

#

default dict type for items its dict_items ie ItemsView from collections.abc. might just need to type hint that for your case. the if TYPE_CHECKING avoids overriding default behaviour.

stiff acorn
#

that seems to be an error?

#
Incompatible types in assignment (expression has type "ItemsView[Literal['one', 'two'], int]", base class "dict" defined the type as "Callable[[], dict_items[Any, Any]]")
lament scaffold
#

ya you might need the Callable[[], ItemsView[xyz, kjsdf]

#

instead of the prev

stiff acorn
#

doesn't work either and as far as I can see, this just types .items()

lament scaffold
#

ah as in you want the type hint on the foo["item"]

#

in that case you might want to type hint the getitem method

#

might be called __getattr__(self,...)

#

cant recall the specific name but that is what python used to get the value for the key given

terse sky
mossy rain
#

Why do you need to make a special hint for the .items method anyways?

terse sky
#

but yeah, a hint on .items() is not going to work anyway because you're going to iterate over everything. so you're not going to be able to offer a more specific type

lament scaffold
#

ya i mean i think what you want is TypedDict but you cant use it or something?

terse sky
#

you can make it so that x = my_dict["one"] gives you a more specific type but not so that x = next(my_dict.items()) has a more speciifc type, afaik

lament scaffold
#

ya that seems like an awfull specific type hint for a itterator. maybe it could be done but ya seems pretty awfull

mossy rain
#

This seems to work (if i understood the problem correctly):

#

just subclass a generic version of dict

lament scaffold
#

actually tbf i type hint dicts like that all the time regularly. dont know why i didnt think of doing that in there like that. dumb

terse sky
#

@stiff acorn

from typing import overload, Literal, Any, reveal_type

class Foo(dict):
    
            
    @overload
    def __getitem__(self, s: Literal["Hello"]) -> int:
        ...
    
    @overload
    def __getitem__(self, s: str) -> Any:
        ...
        
    def __getitem__(self, s): return self.super()[s]

    
    
f = Foo()
x = f["Hello"]
y = f["Bye"]
reveal_type(x)
reveal_type(y)
mossy rain
terse sky
#

probably, don't worry about the actual implementation 🙂

#

the types work though

mossy rain
#

ok, just wondering

restive rapids
terse sky
#

when I was writing the code I couldn't remember how to call the base method in python (I don't do that very often

#

so just put a super there I guess

restive rapids
terse sky
#

I can't use []?

mossy rain
restive rapids
terse sky
#

anyway tbh I don't care very much 😂

#

was just try to help with the typing part

stiff acorn
#

maybe this is an XY problem. I essentially need a typed dict semantics on a real dict subclass, i.e.,:

foo = MyDictSubclass()
foo["bar"] = None # type error because bar: int
baz = foo["baz"] # correctly inferred type of baz
terse sky
#

but for me, if I knew I had some guaranteed, typed stuff there, and some additional stuff

stiff acorn
#

would be nice if i could just do

class MyDic(RuntimeTypedDict):
    baz: int
    bar: str
terse sky
#

My first thought would definitely be a dataclass, with one field as a dict to hold additional stuff

#

but you said you can't use dataclass, so I'm not sure what your exact restrictions are

#

or maybe even

class Point2D(TypedDict):
    x: int
    y: int
    label: str
``` - TypedDict does inherit from dict as well
#

(I believe)

mossy rain
mossy rain
stiff acorn
stiff acorn
terse sky
#

hmm really, that's an interesting restriction

#

where is that restriction described?

#

hmm, it just ignores them

mossy rain
terse sky
#

idk, that's getting pretty hacky

#

I would just do what I had above

#

it's a little boilerplate-y but it works

mossy rain
#

Perhaps try this (idk if you have)?

terse sky
#

You can do this too but it achieves something different from what TypedDict achieves

mossy rain
#

well, there is no way to make explicit key: K -> value: V type mappings, but except for that, it might be enough

#

idk

terse sky
#

thisis just saying that the values will either be str, or "a", or "b". But "a" or "b" are already strings themselves, so this doesn't really add much

#

this approach would be more useful if you know say that all values are either strings or ints, or something like that.

mossy rain
terse sky
#

Gotcha, my b

#

the same argument does apply though

#

str | Literal["a", "b"] is the same as str, as far as the actual type system is concerned

mossy rain
#

depends, it might be enough, cause Literal["a", "b", "c"] != str for type checkers, and the Union Literal["a", "b", "c"] | str does not get converted to str, as far as i know

terse sky
#

Yes, Literal["a", "b"] != str

#

but str | Literal["a", "b"] == str

#

it's not "converted" to str, it will keep the extra annotations around, and it does tell people reading the code something

#

but in terms of the actual type system and type checking, it's the same

mossy rain
#

Then, I guess there is no way to make a 100% working solution, a custom TypedDict + type checker support might be required (i've actually had an idea to simplify adding that support for some time now)...

terse sky
#

saying that the type of the key is str | Literal["a"] doesn't even mean that "a" must be one of the keys

#

well, what I wrote above is a 100% solution 🤷‍♂️

#

in the sense that it works the same way as TypedDict

#

you would need to overload setitem too though 🙁

#

and get...

#

I hope that's everything 🤔

mossy rain
terse sky
#

what does immutability have to do with it?

mossy rain
#

basically, there are a few things that i think are not yet possible to implement in typing 100%

terse sky
#

well, a lot of these issues are not python specific. it's just a fundamental limitation of static typing in many cases.

#

The whole concept of a dynamic collection with heterogenous types over iteration is pretty problematic. Even the Literal stuff is kind of hacky

mossy rain
terse sky
#

i don't know, but that wasn't in the original requirements

#

and python doesn't have anyway to express "set once, but not again"

#

you could make it so that A must be passed at construction time, and then never changed

mossy rain
terse sky
#

is there a way to type a dict subclass similar to a typed dict (eg some keys and their types)
eh, not really

#

a subclass of dict with similar behavior to a typed dict. that's what I have above.

#

though, again, I admit my solution will be a pretty big pain in the ass once you fully implement it

mossy rain
#

OK, then your solution obviously works, the solution is part of the question already

terse sky
#

setitem, getitem, get, setdefault.. oof, 4 repetitions for each field.

#

you can probably justify not having pop and del, I guess

mossy rain
terse sky
#

yeah, AI is good at that sort of thing. It's pretty bad to maintain, but I guess if this is legacy code that doesnt' change very often, and they're trying to add typing to it, could be worth it

summer berry
#

I've had a battle with mypy today writing a decorator that supports both sync and async functions. I think I ran into 3 distinct issues.

One is that inspect.iscoroutinefunction can't use TypeIs. That's cause it won't return true for regular functions that return coroutine objects.

#

It's a tough problem. First, one may not necessarily care whether it's a coroutine function or some other awaitable. But of course it cannot be known at runtime whether a callable should be awaited without first calling it.

#

Second, there isn't a more specific type for a coroutine function (i.e. has the CO_COROUTINE flag)

#

All we have is Callable that returns Coroutine

trim tangle
#

I would personally just make two different decorators, or add some explicit option or something like that

mossy rain
#

yeah, sometimes you gotta lie to checkers, and # type: ignore or cast(...) it. Ive had a function declared to return a string (internal) return an object instead, cause doing x is y is more reliable for objects (users can basically copy the string.

summer berry
trim tangle
#

or the signature at least

summer berry
#

Oops I didn't use the casted function in wraps() but it doesn't make a difference

trim tangle
# summer berry https://mypy-play.net/?mypy=latest&python=3.12&gist=c8864948701e67ae56c02252e043...
def mydec[**P, R](func: Callable[P, R]) -> Callable[P, R]:
    if inspect.iscoroutinefunction(func):
        @functools.wraps(func)
        async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> object:
            return await func(*args, **kwargs)
        return async_wrapper  # type: ignore[return-value]
    else:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            return func(*args, **kwargs)
        return wrapper
#

(the overload is redundant in this case)

summer berry
#

Why? Is AsyncFunc a subtype of Func?

trim tangle
#

in Callable[[int], Coroutine[Any, Any, str]], Coroutine[Any, Any, str] is the "R"

summer berry
#

OK I get it

trim tangle
# summer berry OK I get it

Btw, if you do something like this: py @functools.wraps(func) def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Awaitable[R | SomeError]: if error := some_validation(args, kwargs): fut = asyncio.Future() fut.set_result(error) return fut else: return func(*args, **kwargs) you will need to call inspect.markcoroutinefunction(async_wrapper)

#

because functools.wraps doesn't copy the coroutine-ness automaticall unless you decorate an async def function

summer berry
#

Til about markcoroutinefunction

trim tangle
#

It's your namefellow

summer berry
#

Heh

#

Hmm it's nice to get rid of the overload but then using R as the return type of async wrapper is incorrect

trim tangle
#

Something funky is going on here: ```py
def mydec(func: AnyFunc[P, R]) -> AnyFunc[P, R]:
reveal_type(func)
# Revealed type is "def (*P.args, **P.kwargs) -> R-2 | def (*P.args, **P.kwargs) -> typing.Coroutine[Any, Any, R-2]"

if inspect.iscoroutinefunction(func):
    reveal_type(func)
# Revealed type is "def (*P.args, **P.kwargs) -> types.CoroutineType[Any, Any, Any]"
summer berry
#

That makes me realise iscoroutine function isn't giving a meaningful type to R.

#

Or maybe R becomes CoroutineType[Any, Any, Any]?

#

Which would have the same problem as your proposal above I think

trim tangle
fiery canyon
#

Seems to work.
Was wondering, is it possible to implement fallback syntax for versions below 3.12 conditionally? Since for them this would raise SyntaxError

oblique urchin
#

For type statements you can use typing_extensions.TypeAliasType conditionally on older versions but it's going to be quite verbose

fiery canyon
#

Just upgraded myself to 3.14. Was wondering if I should keep support for old versions xD

fiery canyon
#

Hm

oblique urchin
#

that's fine too, it's expected that the new syntax can only be used in codebases that drop support for 3.11

fiery canyon
#

Only reason for me to keep support is for OS's like some versions of Linux (or all versions of it? Dunno) where for some reason 3.11 is the official latest version.

#

Otherwise I'd say it's the user's problem for not upgrading. Although even on such Linux case you can upgrade manually. Done that recently on my RPI actually

brazen jolt
#

yeah, stable linux distros can sometimes be slow to update python

#

since ubuntu LTS releases have like a 4 year support window

#

and once released, they only get bugfixes / security fixes, so they'll never do an update to python there until the next LTS release

oblique urchin
#

people who are on old versions of e.g. Ubuntu can still get a newer version of Python in various ways (custom PPAs, uv, etc.)

brazen jolt
#

definitely, but generally for public libraries, I tend to make sure they support all python versions that aren't EOL

oblique urchin
#

it's up to you to decide what your version support policy is. Some libraries support all versions still supported by CPython itself (currently that means 3.9 and up, soon to be 3.10 and up only). Others, mostly around scientific python, follow https://scientific-python.org/specs/spec-0000/ which I think currently means 3.12+

Description# This SPEC recommends that all projects across the Scientific Python ecosystem adopt a common time-based policy for dropping dependencies. From the perspective of this SPEC, the dependencies in question are core packages as well as older Python versions.
All versions refer to feature releases (i.e., Python 3.8.0, NumPy 1.19.0; not Py...

fiery canyon
#

Nobody uses the thingy I'm working on atm anyway so I'll drop pre 3.12 syntax lol

#

I'll keep the old syntax commented out

brazen jolt
#

but then again, I just hate "dead code"

fiery canyon
#

I could also do a static check in init and conditionally import different files based on version, no?

brazen jolt
#

but going 3.12+ is perfectly fine

brazen jolt
#

so you still need to care about the old syntax

#

just on top of also having to care about the new one

#

and keeping compatibility

#

since new python versions still support the old syntax without any issues, and that's not going to change any time soon, why bother

fiery canyon
#

You mean I should just use old syntax?

brazen jolt
#

yeah

fiery canyon
#

I won't go beyond 3.10 tho xD

brazen jolt
#

that's perfectly fine, especially for new projects, it's not uncommon to start off at a more recent python version

#

and your versioning policy is fully up to you

#

do what you feel is best, if you get people that want to use your project with lesser version, you can start caring about that

#

but especially for things that you don't expect will get a ton of users and are mostly for your own usage, I would maybe even go 3.13+

fiery canyon
#

Back to the old syntax then (: should i add a TypeAlias annotation? it does nothing on 3.10..

brazen jolt
#

I like to, but you don't need to

fiery canyon
#

It's probably for pre 3.10 isnt it

brazen jolt
#

no, it's for pre 3.12

#

which instead uses the type keyword

#

it's just a nice way to clearly mark off type aliases and regular variables/constants

#

you can also get some diagnostics from a type-checker when adding the TypeAlias type-hint, ensuring that the variable does in fact hold a valid type

#

and you might get different syntax highlighting for type-alias variables

fiery canyon
#

It seemed to do nothing on 3.10.
No runtime problems and my type checker didn't trigger anything whether it's off or on

#

so i assumed maybe in even older versions type checkers cared about it

#

idk

trim tangle
#

It's needed if you want to quote the type alias

#

otherwise I'm not sure

fiery canyon
#

so it has functionality? i dont want to use it for no reason

brazen jolt
#

not on runtime, but for type-checkers, it can

#

like fix error said, when using strings that contain the type annotations, you would have issues with type-checkers understanding that the variable holds a type alias, and not a string

fiery canyon
#

Ah well then I don't need it

#

thx

trim tangle
brazen jolt
fiery canyon
#

markdown is surely cool, though theres much more stuff like html tags xD

brazen jolt
#

I still like using it for non-string type aliases too, just to be explicit about what the variable is

fiery canyon
brazen jolt
trim tangle
#
from typing import TypeAlias

Foo: TypeAlias = "int"

def f(x: Foo) -> None:
    y = Foo()
    reveal_type(y)  # Revealed type is "builtins.int"

# no errors in the file...
``` epic fail...
#

both in mypy and pyright

brazen jolt
#

yeah, allowing it to be used as a type too is a bit... odd

fiery canyon
#

thats one funny "warning"

brazen jolt
#

it's not a warning, it's a debug tool we use with type-checkers, allowing you to see what type the type-checker sees there

trim tangle
brazen jolt
#

it's a diagnostic at info level

brazen jolt
fiery canyon
#

Ah

brazen jolt
brazen jolt
trim tangle
#

They're fine in annotations, I don't think they're supposed to be usable outside of annotations

#

So in my mind Foo() would have to be invalid even if you did Foo: TypeAlias = int

brazen jolt
#

exactly

trim tangle
#

in particular, when you upgrade from the deprecated (!) TypeAlias, you might break code that relied on yourlib.Foo() which type checkers didn't catch

brazen jolt
#

didn't expect pyright/mypy to allow that

trim tangle
#

I'll try making a pyright issue but I feel like it's going to be as-designed-ed

fiery canyon
#

i wouldnt use type aliases for plain types like just int

#

unless that was just a barebone example

brazen jolt
#

typealias isn't exactly new

trim tangle
trim tangle
fiery canyon
#

fasho

brazen jolt
trim tangle
rare scarab
trim tangle
#

pycharm?

rare scarab
#

pylance/pyright actually

trim tangle
#

allow me to interject for a moment...

rare scarab
#

trying to recreate the exact code that triggered it.

rare scarab
#

couldn't do it

trim tangle
trim tangle
#

let the tomatoes fly my way

bleak imp
#

I having trouble understanding how variance is inferred in types.

from typing import Generic, TypeVar

class Ok[T]:
    def __init__(self, value: T):
        self.value = value

x: Ok[object] = Ok[int](1)


OK_T = TypeVar("OK_T", covariant=True)
class Ok_CO(Generic[OK_T]):
    def __init__(self, value: OK_T):
        self.value = value

x_co: Ok_CO[object] = Ok_CO[int](1)

Why is the T in Ok inferred as invariant by default? I looked at the docs on variance inference, but I don't understand what determines when the upper and lower specializations are assignable.

trim tangle
#

This is an extremely frustrating aspect of variance. Type checkers won't tell you why a type is not covariant/contravariant

#

https://mypy.readthedocs.io/en/stable/generics.html#variance-of-generic-types

Here the underscore prefix for _content is significant. Without an underscore prefix, the class would be invariant, as the attribute would be understood as a public, mutable attribute (a single underscore prefix has no special significance for mypy in most other contexts). By declaring the attribute as Final, the class could still be made covariant:

#

I also don't undestand why they don't complain when the explicitly specified inference on typevars is wrong... that's like, silly

bleak imp
#

Huh, I didn’t know that the publicness of a field affects its variance. So with it being a private field, it makes it covariant. At least pyright tells you the variance is part of the issue, but none of them link to info on how to make it right :<

trim tangle
#

The pyright playground is so painfully slow

#

so irritating

#

Even my OG pyright playground wasn't so bad, requests finished within a second or so. And it ran on serverless functions, so it wasn't smart with pre-starting a pool of LSP processes or something like that

trim tangle
bleak imp
#

You need strict on I think

trim tangle
#

nope, doesn't complain about the public attribute assignment

bleak imp
#

Huh? I get

Type "Ok[int]" is not assignable to declared type "Ok[object]"
  "Ok[int]" is not assignable to "Ok[object]"
    Type parameter "T@Ok" is invariant, but "int" is not the same as "object"  (reportAssignmentType)

link

trim tangle
bleak imp
#

Ah, you mean because the variance is wrong

trim tangle
#

yes

#

it does complain if you use a covariant type variable directly in a parameter type, but not if it's part of a larger type or something? How does this make any sense?

class Ok_CO(Generic[OK_T]):
    def __init__(self, value: OK_T):
        self.value: OK_T = value

    def meth1(self, value: OK_T) -> None: ...   # complains here
    def meth2(self, value: list[OK_T]) -> None: ...  # but not here
#

Maybe they just didn't bother implementing any thorough correctness checks in the TypeVar days, and when the new syntax and variance inference came, they didn't add checks that the declared inference is the same as the inferred inference

bleak imp
# trim tangle yes

So then what about this? I guess there's something funny happening with dataclasses in here too, since shouldn't this still work as covariant since _value is private?

from dataclasses import dataclass
@dataclass
class OkData[T]:
    _value: T

x_data: OkData[object] = OkData[int](1)
trim tangle
#

mypy doesn't synthesize a __replace__ method, so it doesn't complain

#

it's been over a year since __replace__ has been in a stable version of Python, but there's still no solution in sight, unfortunately.
Pyright also considers every @dataclass_transform function to add a __replace__ method which it isn't required to do

#

You can set pyright to pretend you're using Python 3.12 and it's going to "fix" that

bleak imp
#

Wow that sucks :/

trim tangle
#

hacks are actually awesome, and it's a shame that pyright doesn't support plugins

#

so apparently the way you add plugins to pyright is by forking it 🥴

static rose
#

how to type hint that dict can have as value str or list of str?

fiery canyon
#

... is key

static rose
#

can i do recursive list? like it can be list that also could have list or str inside?

rare scarab
#

You'll need a recursive type

#
type StrList = str | list[StrList]
type StrListDict[K] = dict[K, StrList]
fiery canyon
#

I'm finally not forced to use quotes yay

weary bone
#

Who will agree that type hinting doesn't really have actual effect, it only helps when documenting a project to help provide actual data type

soft matrix
#

That sounds like an actual effect to me

fiery canyon
#

Says it has no actual effect -> says its exact purpose

#

Like yes during runtime it doesn't do anything, that's how typing works

terse sky
#

fwiw I think documentation is a secondary purpose. the primary purpose is tooling - both static type checking to catch errors, and making your IDE/editor actually work well and offer auto completion and catching many mistakes instantly.

#

and that's definitely an "actual" effect

rare scarab
#

Sad state of types: ```py

used in instanceof, cannot use type statement

JSONPrimitive: TypeAlias = str | int | float | bool | None

recursive, must be quoted or use type statement

type JSONType = JSONPrimitive | list[JSONType] | dict[str, JSONType]

#

If only we had @runtime_checkable for type aliases

fiery canyon
#

runtime_checkable does what

#

Ah well no idea what Protocols are either xD

#

No idea what's the use case for all these

#

Ah I can imagine it actually (replacements for hasattr)

fiery canyon
rare scarab
fiery canyon
#

instanceof?

rare scarab
#

sorry, isinstance.

#

got mixed up with java

fiery canyon
#

xD

rare scarab
#

You can however do isinstance(x, Alias.__value__)

fiery canyon
#

that simple damn

terse sky
#

small tip if you define Json recursive aliases it's good to define a mutable one and a read-only one using Mapping and Sequence

#

for the covariance

#

well, and for the enforcement of not-mutating stuff

feral wharf
#

I feel like i'm missing something obvious here but idk

def _is_page[PageT: (BoundPage | BoundV2Page)](page: PageT) -> TypeIs[PageT]:
    expected_items = (str, discord.Embed, discord.ui.Item, dict, discord.File, discord.Attachment)
    if isinstance(page, expected_items):
        return True

    if isinstance(page, Sequence) and not isinstance(page, (str, bytes, bytearray)):
        return all(isinstance(p, expected_items) for p in page)

    return False

class Foo(...):
    async def _handle_single_page(
        self, page: PageT, /, kwargs: dict[str, Any], items: list[discord.ui.Item[Any]]
    ) -> tuple[dict[str, Any], list[discord.ui.Item[Any]]]:
        if isinstance(page, (int, str, discord.ui.TextDisplay)): ...
        elif isinstance(page, discord.Embed): ...
        elif isinstance(page, discord.File | discord.Attachment): ...
        elif isinstance(page, discord.ui.Item): ...
        elif isinstance(page, dict): ...
        else:
            for _page in page:
                if _is_page(_page): # <- not working?
                    kwargs, items = await self._handle_single_page(_page, kwargs=kwargs, items=items)
#                                                                  ^^^^^^

        return kwargs, items
Argument of type "_BoundPage | _BoundV2Page" cannot be assigned to parameter "page" of type "PageT@BaseClassPaginator" in function "_handle_single_page"
  Type "_BoundPage | _BoundV2Page" is not assignable to type "PageT@BaseClassPaginator"PylancereportArgumentType

bound type aliases and PageT: #type-hinting message

#

-# why do I always bother with typing at late

trim tangle
#

because this type guard is kind of a noop

slender basin
#

Hey. I'm trying to express that a Callable return type is the same as its argument:

from typing import TypeVar
from collections.abc import Callable

class Base:
    ...

class Child(Base):
    ...

T = TypeVar('T', Base)

def apply_func(a: Child, f: Callable[[T], T]) -> Child:
    return f(a)

But all type checkers complain at the last line (f(a)) about a type mismatch. Eg https://play.ty.dev/cee488d8-055b-4c66-be9d-bffad85b8bbe
What am I missing?

trim tangle
#

Do you want the type of f to be: "Expect any type, but at least Base, and return the same type"?

slender basin
#

Usage is something like

c1 = Child()
c2 = apply_func(c1, clone)

And yes this is what I want to express on f

trim tangle
#

Doesn't f need to just work with Child in this case?

#
def apply_func(a: Child, f: Callable[[Child], Child]) -> Child
slender basin
#

In this particular case yes, it is a simplification of the real code. I was hoping type checkers could make use of the type hints here to deduce that f returns a Child

trim tangle
#

If you want it to work with arbitrary subclasses, you need to do this: ```py
class Func(Protocol):
def call[T: Base](self, arg: T, /) -> T: ...

def apply_func1(a: Child, f: Func) -> Child:
return f(a)

def apply_func2(a: OtherChild, f: Func) -> OtherChild:
return f(a)

The way to express the type of a generic function is a "callable protocol"  (a protocol with a `__call__` method)
slender basin
#

So what is even the point of TypeVar?

trim tangle
#

If you are not on python 3.12 yet, then it needs to be this ```py
T = TypeVar("T", bound=Base)

class Func(Protocol):
def call(self, arg: T, /) -> T: ...

trim tangle
slender basin
#

So what is even the point of TypeVar?

trim tangle
#

I'm not sure I understand the question

slender basin
#

I undestand TypeVar as expressing 'Some type (maybe conforming to constraints), but fixed throughout the usage'. Apparently this is not the case, or not entirely

trim tangle
# slender basin I undestand TypeVar as expressing 'Some type (maybe conforming to constraints), ...

Here: py def apply_func[T: Base](a: Child, f: Callable[[T], T]) -> Child: return f(a) means that T is a type variable scoped to the apply_func, not inside the f callable. In TypeScript terms, it would be like this: ```ts
function applyFunc<T extends Base>(a: Child, f: (arg: T) => T): Child
// NOT like this:
function applyFunc(a: Child, f: <T extends Base>(arg: T) => T): Child

So it's valid to call `apply_func` with e.g. these functions as `f`:
```py
def f1(b: Base) -> Base: ...
def f2(b: Child) -> Child: ...
def f3(b: OtherChild) -> OtherChild: ...
trim tangle
#

That's not my suggestion, that's just your example using the newer syntax

#

it has exactly the same meaning as ```py
T = TypeVar("T", bound=Base)
def apply_func(a: Child, f: Callable[[T], T]) -> Child:
return f(a)

#

I'm explaining that f: Callable[[T], T] is not defining a requirement that f must be a generic function

slender basin
#

I understand, I was wondering what is the point of TypeVar (in either old or new generic syntax), if type checkers cannot deduce that the return type is identical to the received argument type

trim tangle
# slender basin I understand, I was wondering what is the point of TypeVar (in either old or new...

A TypeVar is resolved to some specific type when you call a function.
Here's an example of correctly using type variables:

T = TypeVar("T")

def run_func(argument: T, func: Callable[[T], int]) -> int:
    return func(argument)

def does_contain(element: T, items: list[T]) -> bool:
    for item in items:
        if item == element:
            return True
    return False

x = run_func("aaa", len)  # x is an `int`, T is resolved as `str`
y = run_func("bbb", len)  # y is an `int`, T is resolved as `str`
z = run_func(42, len)  # T is resolved as `int`, but `len` doesn't accept `int`, so this is not allowed

xs: list[int] = [1, 2, 3]
t0 = does_contain(42, xs)  # T is resolved as `int`, ok
t1 = does_contain("ccc", xs)  # error, cannot resolve `T` such that "ccc" is a T and xs is a list[T]
slender basin
#

I think I understand. Thank you!

trim tangle
#

unfortunately callable protocols like this are not explained in detail anywhere, so I can understand the frustration

slender basin
#

Indeed.

wild valley
#

on the typing of async generators: is every AsyncGenerator annotation expected to contain None in its send type?

#

(the second type argument)

#

because otherwise I don't see how await agen.asend(None) would be legal from the typing perspective

#

and this is required to start any async generator (you have to send it None as the first item regardless of how you use it)

#

I asked AI and it gave me the answer I expected: this is an incoherent, unfixable mess

trim tangle
#

did you ask it about just this, or typing in general? 😛

wild valley
#

I asked it that specific question

#

with a follow-up

#
#

in my case I was actually able to figure out a workable solution:

elif item_to_send is None:
    item_from_agen = await self._asyncgen.__anext__()
else:
    item_from_agen = await self._asyncgen.asend(item_to_send)
trim tangle
wild valley
#

that's what I did at first but I strongly dislike type ignore comments

trim tangle
#

Well, __anext__ existing on every AsyncGenerator is a similar kind of soundness hole

wild valley
#

no argument there

trim tangle
#

but yes, # type: ignore can hide errors you didn't intend to hide

#

You could do something like this, though I assume it only works in CPython ```py
def start_gen[T](gen: Generator[Any, T, Any]) -> T:
assert not (gen.gi_suspended or gen.gi_running)
return next(gen)

#

basically, ensure that the generator isn't started yet and only then start it

feral wharf
#

That shouldn't interfere with anything?

trim tangle
# feral wharf Wdym?

Well, this type guard is pretty useless ```py
def is_foo[Foo](x: Foo) -> TypeIs[Foo]

#

If you're using typevar constraints instead of bounds, I'd be a bit more trigger-happy about typing.cast or # type: ignore because that feature is a bit janky

feral wharf
trim tangle
#

you can rename PageT in it to Foo and it will have the same effect

feral wharf
#

Wait what

#

Does it not uh "pass" that when calling it?

#

Wait

#

I'm so confused lmfao

#

So what I thought it was supposed to do is

page = BoundPage | BoundV2Page
the function makes sure of that
-> return it's 100% BoundPage | BoundV2Page

PageT = (BoundPage, BoundV2Page) so it's fine

I see the misunderstanding from me here, but no idea how to fix it

#

So you suggest just ignoring it because it's janky as you've already explained before

trim tangle
#

though in your case, that probably won't help, since you need to check whether it's specifically the same type as your bound variable

#

so yeah I'd just use typing.cast and move on to more interesting tasks

feral wharf
#

fair, thanks again!

soft matrix
#

anyone (cough cough @trim tangle) ever had the idea to try doing some kind of unit type using vectors or is that too out of the reach of possibility right now?

trim tangle
#

jesus, wear a mask

soft matrix
#

I think you could do some nasty stuff with TypeVarTuple for it maybe

trim tangle
#

You mean something like foo: Kilograms

#

generally, if your question is Is this fun and useful thing possible with TypeVarTuple, the answer is probably no

soft matrix
#

well yes but Kilograms would be an alias to something like Units[0, 0, 1, 0, 0, 0, ...] https://en.wikipedia.org/wiki/International_System_of_Units#SI_base_units I'm mainly interested in the dimensional analysis side of it

The International System of Units, internationally known by the abbreviation SI (from French Système international d'unités), is the modern form of the metric system and the world's most widely used system of measurement. It is the only system of measurement with official status in nearly every country in the world, employed in science, techno...

trim tangle
#

and it can also be fractional

soft matrix
#

oh yeah guess its impossible cause no literal floats

trim tangle
#

that's fine, we can use a tuple to represent a fraction

#

we can't do arbitrary addition or subtraction in the type system though

soft matrix
#

it would need some kind of vector multiplication at type time

#

but i remember seeing you do some wacky stuff with typevartuple

#

alas maybe one day

restive rapids
#

typing.Literal with no way to manipulate literal values in the typesystem is a mistake
its not like the typecheckers dont already effectively (re)implement python, it wouldnt be hard to add computation of compile-time values

trim tangle
#

We need to work on adding a lispy microlanguage into basedpyright

restive rapids
#

inside every sufficiently complex static analysis tool there is a partial implementation of the thing its analysing waiting to burst out

jolly cipher
#

found a super interesting case with structural types as constraints.

this doesn't pass type checking in pyright & mypy and only passes in ty:

from typing import Generic, Protocol, TypeVar

class Fooable(Protocol):
    def foo(self) -> None:
        ...

class Barable(Protocol):
    def bar(self) -> None:
        ...

class Both(Protocol):
    def foo(self) -> None:
        ...
    def bar(self) -> None:
        ...

T = TypeVar("T", Fooable, Barable)

class G(Generic[T]):
    z: T

g = G[Both]()
reveal_type(g)  # ty: G[Both], pyright & mypy: G[Fooable]
g.z.bar()  # ty: ok, pyright & mypy: error

because both pyright and mypy reveal G[Fooable], because it was the first that matched!

now if i change the order of constraints:

-T = TypeVar("T", Fooable, Barable)
+T = TypeVar("T", Barable, Fooable)

pyright and mypy allow this because they reveal G[Barable]!

#

i'm not happy with how any of type checkers handles this.
i believe that it shouldn't be possible to bind T to Both, nor that Both should be interpreted as the first matching type from the constraints.

#

@oblique urchin wdyt?

#

this is very theoretical, but just exploring.

oblique urchin
#

yeah this is a very hard edge case

#

arguably ty is wrong here because T can only solve to Fooable and Barable and nothing else

#

but then there's no way to prefer either over the other so what can you do

#

not sure if ty made a principled choice here or this is accidental behavior

trim tangle
#

I think G[Both] should be illegal. Only G[Fooable] and G[Barable] should be legal

#

allowing this ```py
T = TypeVar("T", str, int)

class G(Generic[T]):
z: T

x = GLiteral["foo"]

and "fixing it up" as `G[str]` will just confuse people who don't understand the difference between typevars with a constraint and a bound
#

(which is common)

mossy rain
#

Is there a reason why Literals cant be floats, but ints are accepted? In PEP 586, it says Floats: e.g. Literal[3.14]. Representing Literals of infinity or NaN in a clean way is tricky; real-world APIs are unlikely to vary their behavior based on a float parameter., but there are no sources to this in any way. Some API's could perhaps say that if a timeout < 0.0, we return Never (pretty obvious, but type-checkers won't raise errors i guess)?!

pastel egret
#

Well first Python doesn't have float literals for infinity or NaN, so we'd either have to special case math.nan/inf, allow float("inf")' inside Literal`, or something else. Your example shows the second part - you'd need a comparison, not a literal value. Unlike integers you can't really list all the values. Though, I do agree it would be nice to have something for this, even just for nan/infinity.

#

Oh also the problem of negative infinity.

lunar dune
mossy rain
pastel egret
#

Well that's the trouble really, what floats, how much precision?

#

Since you're almost always doing comparisons, not equality checks.

trim tangle
#

literals only account for equality, not ranges

#

you can't say Literal[> 0.0]

jolly cipher
#

wrote a stub generator with Literal consts for the errno module, output: https://github.com/bswck/generate-errno-stub/actions/runs/18749599178/job/53485540693#step:4:5.
this however should be run on other platforms than linux, windows & macos (errno.pyi has some fragments specific to other platforms)
wondering if we could apply this workflow (after tweaks) to some other typeshed stubs as well

GitHub

Contribute to bswck/generate-errno-stub development by creating an account on GitHub.

#

this assumes that an errno that is present in linux, windows, macos with identical values (literal ints) will exist in all supported platforms with these exact values (literal ints)

#

cc @oblique urchin

oblique urchin
jolly cipher
#

perhaps only those specialized

oblique urchin
#

yes, I think we've been adding Literals for a bunch of these

#

or maybe we decided that we wanted ESOMETHING: Final = 42, can't recall

jolly cipher
#

(in general)

terse sky
#

in pyright it will be Literal[42] in both cases I believe

trim tangle
#

yes, type inference like that is not specified, so for type stubs or library exports you may need to annotate explicitly

mossy rain
#

i perhaps didnt explain the issue i have good enough, but im basically just asking, why Literal[<some_float>] is invalid, when we could say its valid for everything thats not nan, +inf, -inf

trim tangle
#

The pep that introduces Literal says: https://peps.python.org/pep-0586/#illegal-parameters-for-literal-at-type-check-time

The following are provisionally disallowed for simplicity. We can consider allowing them in future extensions of this PEP.

  • Floats: e.g. Literal[3.14]. Representing Literals of infinity or NaN in a clean way is tricky; real-world APIs are unlikely to vary their behavior based on a float parameter.
#

do you have some use case for this?

mossy rain
mossy rain
trim tangle
#

Why does it have to be a literal?

mighty heart
#

python3 -c 'print("helloworld")'
what's wrong in the script?

restive rapids
mossy rain
# trim tangle Why does it have to be a literal?

How are Checkers gonna know the value (0.0) else? Some might check the code itself (e.g. if i do DEFAULT_TIMEOUT: ... = 0.0), and infer that something like if DEFAULT_TIMEOUT < 0.0: ... is unreachable, but most checkers will not go that far. If i were to make it an int however, e.g. DEFAULT_NUMBER: Final[Literal[0]], then checkers should be able to know that similar code is unreachable (idk how advanced all checkers are. Apart from that, users that only have access to the stubs (e.g. if i decide to write parts of the API in C), will be able to see that the default value is 0.0, without me having to create any comments or such.

trim tangle
#

Can you show the code maybe? I don't think I follow

#

Ah, I see. You want reachability analysis when directly using the value

mossy rain
#
from typing import Final, Literal


__all__ = [
    "DEFAULT_PORT",
    "DEFAULT_DATABASE_PATH",
    "DEFAULT_TIMEOUT",
    "DEFAULT_METHOD",
]


DEFAULT_PORT: Final[Literal[8000]]
DEFAULT_TIMEOUT: Final[float] = 0.0
DEFAULT_DATABASE_PATH: Final[Literal["project.db"]]
DEFAULT_METHOD: Final[Literal["get"]]``` See how the only value, where i have `= ...` is `DEFAULT_TIMEOUT`
mighty heart
#

oh

#

so i will type it like this
python3 -c ("helloworld")

bleak imp
#

Is there a type safe way to take in a tuple[T, Sequence[T]] and flatten it into a Sequence[T]? My intuition is telling me no because of types like str that are sequences of themselves, but that really sucks since this would be very nice functionality to have.

oblique urchin
bleak imp
#

Oops, yeah I meant tuple[T | Sequence[T], ...] phone hard

oblique urchin
#

yes, that's inherently difficult to type safely

#

you can do the obvious thing (def f[T](x: Iterable[T | Iterable[T]]) -> Sequence[T]: ... or similar and most types will probably be inferred the way you want in practice

bleak imp
# oblique urchin you can do the obvious thing (`def f[T](x: Iterable[T | Iterable[T]]) -> Sequenc...

The definition works fine at least in concept, but (aside from wrangling the sea of generic soup in my actual code) I'm struggling to make the implementation work. Trying the first thing that comes to mind:

from collections.abc import Sequence


def flatten[T](seq: tuple[T | Sequence[T], ...]) -> Sequence[T]:
    output: list[T] = []
    for item in seq:
        if isinstance(item, Sequence):
            output.extend(item)
        else:
            output.append(item)
    return output

basedpyright rightfully complains that output.extend(item) doesn't work, since item could either be the desired Sequence[T@flatten], or T itself could be a Sequence, resulting in the Sequence[Unknown], and I'm not sure how this would be solvable.

viscid spire
#

you would need negative types, right?

trim tangle
#

yep

#
tuple[T & ~Sequence[Any] | Sequence[T], ...]
viscid spire
#

typescript has Exclude<T, U> for this

viscid spire
bleak imp
viscid spire
#

or well, I guess you can still accept sequences inside of the sequence... and they aren't flattened

oblique urchin
trim tangle
#

I wonder if T & ~Sequence[Any] would mean

T which is not a sequence of some specific, but unstated, static type
or
T which, for any static type X, is not a Sequence[X]

oblique urchin
#

A ~Sequence[Any] could still be some kind of Sequence

#

(which means I think it should mean @trim tangle's first option)

oblique urchin
trim tangle
#

I think the way the spec talks about Any is very esoteric and probably not how Python developers think about Any

viscid spire
trim tangle
#

and there's also the caveat that it introduces unstated type variables with dependencies on runtime values or something like that

bleak imp
#

At least for my use cases strings being sequences of themselves doesn’t really matter, it would work fine either way

oblique urchin
viscid spire
oblique urchin
#

though some of the typing literature does

trim tangle
# oblique urchin the spec doesn't define Any in terms of typevars

Maybe I'm misremembering, but I asked some time ago about this: py def set_attribute(obj: object, key: str, value: Any) -> None: setattr(obj, key, value) it doesn't make sense to say that Any refers to some specific, but unstated, static type. Since the type varies depending on what key is

bleak imp
#

But I am also find out that this idea was probably a bust from the start, since I think actually typing the type of this in a useful way with my current implementation would require higher kinded type bounds (which also don’t exist yet)

trim tangle
#
def flatten(seq: Iterable[Any]) -> list[Any]:
viscid spire
#

keep in mind that Sequences are not mutable

bleak imp
#

My implementation is actually a class that passes a self instance into another class that then is returned and does the processing (generic soup), and I don’t think with all my genetics I can actually express the self bound

#

But I can just split it up into four less powerful methods that handle the four len 2 tuple cases, which should be enough to get the job done

viscid spire
#

or use overloads in the typing

bleak imp
#

Am I doing this correctly? I just recently found out about TypeVarTuples, and was excited to use them to make some code less spaghetti, but it looks like BasedPyright is not happy. Is this a bug in BasedPyright, or am I doing it wrong? playground
All the other type checkers also experience errors in completely different places (ty doesn't have support yet, mypy accepts this but dies on a more complex example, pyrefly gives a completely different error at Ok(func(*self._value)) ), so is this just something that's not fully supported / too spaghetti for current type checkers? Or am I just doing something wrong?

little hare
#

Will report tomorrow but I think I found a bug where if you merge an unknown type with a literal string you get a literal string with pyright

viscid spire
bleak imp
viscid spire
#

I am using strict with pylance LSP

viscid spire
little hare
#

Pylance isn't necessarily the latest pyright version

viscid spire
bleak imp
viscid spire
#

that has nothing to do with type var tuples

bleak imp
#

At least in theory, the type var tuple should allow for the lambda type to be infered

#

And it also works fine in just the Ok case, so type inference does work to some extent, but adding in the Err case breaks it

#

playground No errors if I switch the function to just Ok instead of Result

viscid spire
#

ok well that is because in Err.star_map_ok(), you did not establish a relationship between the class and the type var tuple

#

so there is nothing to determine it

#

you only reference *Ts once there, in the args of _func, so when you pass in an untyped function, it doesn't know

#

@bleak imp ?

bleak imp
#

So is there any way to fix that? I'm trying thing now to see if I can

viscid spire
#

the best way would be to just not have that method on Err?

#

actually I may have misunderstood the design

bleak imp
#

It's trying to model Rust's Result type, so having the method on both Ok and Err is the entire point, since you'd only use this on a combined Result.

viscid spire
#

I see

bleak imp
#

(Not shown in the example is the equivalent function star_map_err, which does the same thing reversed: Map the error value if it's Err, and does nothing on Ok)

viscid spire
#

I'm trying to figure out how to accept a lambda when the type doesn't matter, but it likes to complain that the type is not known

#

because that's the real issue

#

here's an MRE

#

mypy (after adding a return type to f) is fine with it on strict

bleak imp
#

A couple things:
I also just came to the same conclusion, since the same issue happens with map_ok, so the type var tuple isn't at fault.
And I found this solution, which seems to work, though I'm not sure if it's type safe: playground

viscid spire
#

you have another issue btw

Final name declared in class body cannot depend on type variables
bleak imp
#

That one's mypy exclusive so I'm pretending it doesn't exist

viscid spire
#

yeah not sure why it exists

bleak imp
#

I'm still not convinced thowing nevers at it is the correct solution, but it seems to work for the star case as well: playground

#

So then I guess the next question is: Is there something I can do instead of ```py
type SuperNeverCallable = Callable[[Never], Never] | Callable[[Never, Never], Never] # And then put like a billion more versions here

? Since the obvious-seeming `Callable[..., Never]` doesn't work
viscid spire
#

the problem is that ... is not Never, but rather, Any (or maybe object)

bleak imp
#

I remember reading this somewhere, Callable[..., T] is the same as def foo(*args: Any, **kwargs: Any) -> T

viscid spire
#

yeahhh

#

oh wait

#

just define a protocol

bleak imp
#

Oooh that's smart, but doesn't work :(

viscid spire
#

yeah wtf

bleak imp
#

I wonder why it doesn't though, these error messages are super confusing:
BasedPyright:

Argument of type "(x: Unknown, y: Unknown) -> Unknown" cannot be assigned to parameter "_func" of type "SuperNeverCallable" in function "star_map_ok"
  Type "(x: Unknown, y: Unknown) -> Unknown" is not assignable to type "(*args: Never, **kwds: Never) -> Never"
    Parameter "*args" has no corresponding parameter
    Parameter "**kwds" has no corresponding parameter  (reportArgumentType)

Mypy:

main.py:27: error: Argument 1 to "star_map_ok" of "Err" has incompatible type "Callable[[Any, Any], int]"; expected "SuperNeverCallable"  [arg-type]
main.py:27: note: "SuperNeverCallable.__call__" has type "Callable[[VarArg(Never), KwArg(Never)], Never]"
#

Maybe it has to do with the variance of args in callables? But then it doesn't make sense why the union method works

#

I think for that to work the args would have to be covariant, but I remember reading that function args are contravariant instead (but that still doesn't explain why the unions work. Special typing inside the type checker?)

viscid spire
#

awesome

#

it doesn't work because pyright thinks that FunctionType.__call__ is a Callable[..., Any]

bleak imp
#

That just might be display jankery, I think the protocol callable definition has to work normally

#

Another fun thing is that mypy doesn't accept the union method, so I guess it's a pyright specific special case?

bleak imp
#

Hm, now I'm even more confused. Mypy does accept this code, despite the fact that it rejects the other usage:

from typing import Callable, Never
def foo(_x: Callable[[Never], object]):
    ...
def bar(_x: int): ...
foo(bar)
viscid spire
#

you aren't passing a lambda

bleak imp
#

The types should be incompatible though

viscid spire
#

should they?

#

it means that you can't pass an arg to _x when you use it inside of foo (or well, call it at all then)

bleak imp
#

Or wait, yeah that makes sense, since it's with respect to the type in the function

#

And yes, this works ```py
class SuperNeverCallable(Protocol):
def call(self, *args: Never, **kwds: Never) -> object: ...

viscid spire
#

oh self

#

I'm 'tupid

bleak imp
#

Contravariant in the inputs, covariant in the outputs, so Never for the inputs, and object for the output.

viscid spire
#

I forgot to put self

viscid spire
bleak imp
viscid spire
#

you didn't turn strict on

bleak imp
#

all is more strict than strict

viscid spire
#

oh hmm

#

in pylance on my vscode, strict is the top strictness

bleak imp
#

BasedPyright adds more new rules

viscid spire
#

ah ok found it... you didn't use SuperNeverCallable

#

you still have a Callable[...] in your Err class

bleak imp
#

;-;

viscid spire
#

race car emote

#

;-;

bleak imp
#

I have too many playgrounds open and got confused

viscid spire
#

to me it just seems like a bug in how FunctionType.__call__() is handled

bleak imp
#

Hm, ty also agrees with this https://play.ty.dev/a497c7d9-2377-41e1-a36c-2e781fee0466

from ty_extensions import static_assert, is_assignable_to
from typing import Any, Callable, Final, Never, Protocol

class SuperNeverCallable(Protocol):
    def __call__(self, *args: Never, **kwds: Never) -> object: ...

# Static assertion error: argument of type `ty_extensions.ConstraintSet[never]` is statically known to be falsy
static_assert(is_assignable_to(Callable[[int], None], SuperNeverCallable))

So I wonder what we're missing, since all the checkers agree on it

viscid spire
#

did you get the order of the arguments right in the in_assignable_to?

#

(just wanna check)

bleak imp
#

Yep, someone finally added docstrings and better arg names just this last release to all the ty_extensions functions

viscid spire
#

aight

#

I kinda feel like all these typecheckers are wrong on this point

#

but I'm sure Jelle could tell me why they are right

bleak imp
#

Or I guess the more correct one would be is_subtype_of, since we are dealing with fully static types here: https://play.ty.dev/d0af2466-e44b-47e9-8462-f7deeac0b3bc

from ty_extensions import static_assert, is_assignable_to, is_subtype_of
from typing import Any, Callable, Final, Never, Protocol

class SuperNeverCallable(Protocol):
    def __call__(self, *args: Never, **kwds: Never) -> object: ...

# Static assertion error: argument of type `ty_extensions.ConstraintSet[never]` is statically known to be falsy
static_assert(is_subtype_of(Callable[[int], None], SuperNeverCallable))

But still doesn't work

viscid spire
bleak imp
#

I wonder if there's some existing typing discussion around functions with Never *args and **kwargs, since I remember the Any case having some special casing, but that probably doesn't apply to Never

viscid spire
#

alright that's enough from me

#

GL

bleak imp
#

Yep, I should go to sleep, so I'm hoping one of the typing wizards sees this later and can help understand where we went wrong

mossy rain
#

Why do you guys use typing.Callable? Its been deprecated (for checkers), no?

fiery canyon
#

It still works technically

mossy rain
#

But checkers should error if they see it (atleast the newest versions)

fiery canyon
#

Mine doesn't idk

mossy rain
fiery canyon
#

Yes it is

mossy rain
#

do you have some strict mode enabled?

fiery canyon
#

I'm on standard, maybe that's why it doesn't care

mossy rain
#

This is what i get:

fiery canyon
#

Lemme see if it changes when I'm on strict

mossy rain
#

should, perhaps your version is outdated though?

fiery canyon
fiery canyon
mossy rain
#

i forgot i have like 300 lines of custom config for every checker i use, and just assume thats standart lol

#

(tbh, i just turn everything to the most strict setting there is, and if my code has no errors, its truly good (or has a few type: ignore comments xd))

fiery canyon
#

Welp this is why I didn't wanna turn strict on

#

Aw mane

mossy rain
#

Here, i'd suggest a dataclass though, that should work best

fiery canyon
fiery canyon
mossy rain
mossy rain
#

something like that should work

fiery canyon
#

That's just static

mossy rain
fiery canyon
mossy rain
#

yeah

#

Where do you define the data dict?

fiery canyon
#

I can also make JsonDict a typeddict

mossy rain
fiery canyon
#

its from a payload

mossy rain
fiery canyon
#

ah

#

right

#

Not sure I wanna do this though, pretty much doubles my syntax

#

idk

mossy rain
#

Well, sadly hinting always adds a ton of 'unnecessary' stuff

fiery canyon
mossy rain
#

You mean a normal class inheriting from it, or a dataclass that inherits from another dataclass?

fiery canyon
#

actually i think i have a way to solve the first problems

mossy rain
#

nice

fiery canyon
#

I can keep it a normal class with __init__ if I follow this logic

#

But it only works if I type-wrap each thing.

#

Due to that, we still have an issue when it's a dict

#

Because I can't wrap it

mossy rain
fiery canyon
#

In what way tho

mossy rain
#

For me, this works: ```py
from dataclasses import dataclass

@dataclass
class c:
a: int
b: str

print(data := c(a=100, b="hi"))
print(data.a)

#

and due to kwarg logic being allowed at initiation, i think c(**orig_data) should work

fiery canyon
#

Hmm should I just make all of these be dataclasses

mossy rain
fiery canyon
#

I can also use typing.cast

#

Though im not a fan of all these tricks in simple objects

mossy rain
#

Yeah, i try to avoid casting as much as possible too, its kinda like cheating

fiery canyon
#

the only way to fix this without cheating would prob be dataclass for each object

mossy rain
#

How many dict-like objects do you have?

fiery canyon
#

Objects that take dict?

#

Or inner objects that are unparsed and are dicts

mossy rain
#

Objects that are a dict

fiery canyon
#

Like user and guild here?

#

🥀

feral wharf
#

dict[str, Any] sparkle_blurple

#

-# also consider msgspec structs over dataclasses

fiery canyon
#

time to google what that is

fiery canyon
#

Oh type checker on strict triggers when you use _privates

#

That's cool

#

It says unecessary instance calls which is true but I literally made that check for users without typecheckers..

#

Should I:

  1. Add type ignore
  2. Remove and let users eventually realize theyre doing something wrong
mossy rain
fiery canyon
#

ah u can do it specifically like that

#

damn

mossy rain
#

or make it into a function, like ```py
def is_valid(event_type: Any) -> bool:
return not (isinstance(event_type, type) and issubclass(event_type, events.AnyEvent))

fiery canyon
#

nah no need

fiery canyon
#

or is it for clarity

mossy rain
#

so now if you pass some invalid event_type, or something, it tells you

fiery canyon
#

ah so i gotta spell it the exact way, in that case it's reportUnnecessaryIsInstance ig

#

can i pass multiple of those to the []

#

hrmmmm

mossy rain
fiery canyon
#

if i change type to pyright it does work lol

mossy rain
#

ah yeah, forgot that, sadly there is no exact standart all checkers use

#

(would be cool tho)

fiery canyon
#

But tbh this is kinda pointless when that line is always static xD

#

might as well just put type ignore normally

#

but its good for clarity

#

Bruh (using 3.12+ type hinting fixes this)

#

just not sure i wanna migrate to the newer typing, idk who might use the code

mossy rain
#

Well, if HandlerFunc is generic, just use it as a generic piece of code. If you still want a "placeholder" value, parameterize it with Any, or perhaps make the AnyEvent = TypeVar("...", default=events.AnyEvent)

#

the default should then automatically be applied everywhere, unless you do HandlerFunc[...]

fiery canyon
#

what's the diff between bound and default

#

bound means i cant use it in any way but x?

mossy rain
#

Bound means that the typevar can only be of that type's bound, yes

#

but default means, that if you dont pass anything for the typevar, it "replaces" itself with the dafault

#

like type G[T] = list[T], where T = TypeVar("T", default=int) means that G == G[int]

fiery canyon
#

Alrighty thanks

mossy rain
#

np

fiery canyon
#

would it be better to rename it to AnyEventT

mossy rain
trim tangle
#

if you're targeting 3.12+ (which seems reasonable for a new library), just don't use TypeVar

mossy rain
fiery canyon
#

Not for my case

#

If I can safely assume enough people migrated to post 3.12 then i should migrate my typing

trim tangle
mossy rain
trim tangle
#

yes, variance is inferred automatically

#

and typing_extensions.TypeVar now has an infer_variance parameter

mossy rain
#

vsc doesnt show that, and im too lazy to print all that, so i never noticed lol

trim tangle
#

🤔 that's from pyright, so you should get this when you hover over T

fiery canyon
#

type HandlerFunc[AnyEvent: _AnyEvent] = Callable[[AnyEvent, datetime], Coroutine[None, None, None]]

type HandlerFunc = Callable[[_AnyEvent, datetime], Coroutine[None, None, None]]

#

Will both work the same way

#

Well second seems to work just fine

mossy rain
trim tangle
#

maybe, not sure

#

You can always test it in a crude way like ```py
def f(x: c[int | str]):
t0: c[int | str | None] = x # if this fails, x is not covariant
t1: c[int] = x # if this fails, x is not contravariant

fiery canyon
#
def on_event[AnyEvent: _AnyEvent](self, event_type: type[AnyEvent], /) -> HandlerFuncDecorator:```

```py
@app.on_event(events.ApplicationAuthorized)
async def foo(event: events.ApplicationAuthorized, time: datetime):
   ...

I get "_AnyEvent" is not assignable to "ApplicationAuthorized" (ApplicationAuthorized subclasses _AnyEvent)

trim tangle
#

HandlerFuncDecorator needs to be geneirc in AnyEvent

fiery canyon
#

So Decorator should take what as generic?

#

Should they both be generic?

#

Both being generic: works (oof so much repeating syntax), except for one thing:
self._event_handlers: dict[str, HandlerFunc[_AnyEvent]] = {} it's outside of the function that takes the generic "AnyEvent"

And then it causes a problem with this line, that is in the function that takes the generic "AnyEvent":
self._event_handlers[reversed_map[event_type]] = func

The error:
Type "_AnyEvent" is not assignable to type "AnyEvent@on_event"

#

Might just make event_handlers's type hint take Any (or ... ?)

#

And regardless, there's another problem:

self._EVENTS_MAP: dict[str, type[_AnyEvent]] = {
      "APPLICATION_AUTHORIZED": events.ApplicationAuthorized,
      ...
}```
```py
event_cls = self._EVENTS_MAP[event_type]
event = event_cls(event_data)```
"Expected 0 positional arguments" - also caused by the type hint of EVENTS_MAP. Idk what to change here
#

Should I just use Any in those places

trim tangle
trim tangle
fiery canyon
trim tangle
#

?

fiery canyon
#

Its only purpose is for the generic handling

trim tangle
#

If you want the __init__ of event classes to have a certain shape, you need to define that

#

Or you could make a classmethod, especially if event_data is some raw untyped and unvalidated data

fiery canyon
trim tangle
#

Yes

fiery canyon
#

What'd i put there lol

trim tangle
#

Well, what do you want the interface of an _AnyEvent's __init__ to be?

#

Seems like you want it to accept a single argument of some specific type

fiery canyon
#

yep (but in runtime when i do event = event_cls(event_data) im pretty sure it goes directly to the specific object that subclasses _AnyEvent)

#

🤔

trim tangle
#
class _AnyEvent:
    def __init__(self, event_data: object, /) -> None:
        # override this in a subclass
        pass
stiff acorn
#

Sqlalchemy has a func namespace which allows you to call functions defined in your database (e.g., func.count() for COUNT). Sqlalchemy is typed and func is as typed as it can be since it obviously can't know what built in functions every database offers so it uses __getattr__ for these. Is there anyway to tell typecheckers that this specific object has X, Y, and Z methods because I know my database has those?

fiery canyon
#

Maybe assert isinstance(var, inst) ?

stiff acorn
#

TLDR: supplementary type information for a third party object?

fiery canyon
#

uh ig

mossy rain
#

@fiery canyon i'd suggest you make _AnyEvent an ABC, and delete it too in events.py. I've send a PR for that.

fiery canyon
#

Or whats its purpose

mossy rain
fiery canyon
#

Unless thats not how it works

mossy rain
fiery canyon
#

Dw 🙏🏻

mossy rain
#

Ok, should be done now

fiery canyon
mossy rain
#

cool

feral wharf
#

Shouldn't you use a Protocol over abc in modern Python?

rare scarab
#

both have their uses

fiery canyon
#

Wouldn't protocol only be useful when it is not empty

feral wharf
#

true

drowsy wadi
mossy rain
rare scarab
#

Though ABCs also add a metaclass, which isn't always wanted.

mossy rain
#

yeah, and you need another import (protos would only need typing, which should be pretty common in hinted files)

rare scarab
#

You have to do extra work to use them with a custom meta. ```py
import abc

class MyABCMeta(abc.ABCMeta): pass
class MyClass(metaclass=MyABCMeta): pass

mossy rain
#

also can you even define a custom meta for protos? Like they have to subclass other protos, so i'd guess there are some rules for that too

rare scarab
#

Protocol doesn't have a meta

mossy rain
rare scarab
#

metaclass=type is the default

mossy rain
#

And this too

#
from typing import Protocol

class meta(type):
    @staticmethod
    def meth(a: int, b: int) -> str: ... 

class Proto(Protocol, metaclass=meta):
    pass

Proto.meth(1, 2)```
#

(altho again that would be pretty stoopid to make, its just an example)

mossy rain
hasty phoenix
#

Is there a way to tell that a var inherits the type from another? In this example fakeprint is something that behaves like print().

def fakeprint(*args, **kwargs):
    return None
myprint = print if use_real else fakeprint
# ^ How to ensure `myprint` ony use the signature of print
mossy rain
#

You can simply copy the annotations of print, which are found in builtins.pyi (typeshed)

mossy rain
rough sluiceBOT
#

stdlib/builtins.pyi line 1801

@overload```
mossy rain
trim tangle
#

I think our bot doesn't parse the C modifier and just assumes you meant #L1801

rough sluiceBOT
#

stdlib/builtins.pyi lines 1801 to 1812

@overload
def print(
    *values: object,
    sep: str | None = " ",
    end: str | None = "\n",
    file: SupportsWrite[str] | None = None,
    flush: Literal[False] = False,
) -> None: ...
@overload
def print(
    *values: object, sep: str | None = " ", end: str | None = "\n", file: _SupportsWriteAndFlush[str] | None = None, flush: bool
) -> None: ...```
mossy rain
#

should i try and fix that?

trim tangle
#

that sounds useful, yes

mossy rain
#

k

hasty phoenix
#

I think I know how @overload is used (at least the most basic use), but I'm not sure how to apply that to my fakeprint function. But I can search around to see how it might be done. Thanks.