7 ms·
I actually don’t like python type hints! At my work we have a jit compiler that requires type hints under some conditions. Aside from that, I avoid them as mu
by Dr_Birdbrain 1y ago
I actually don’t like python type hints!
At my work we have a jit compiler that requires type hints under some conditions.
Aside from that, I avoid them as much as possible. The reason is that they are not really a part of the language, they violate the spirit of the language, and in high-usage parts of code they quickly become a complete mess.
For example a common failure mode in my work’s codebase is that some function will take something that is indexable by ints. The type could be anything, it could be List, Tuple, Dict[int, Any], torch.Size, torch.Tensor, nn.Sequential, np.ndarray, or a huge host of custom types! And you better believe that every single admissible type will eventually be fed to this function. Sometimes people will try to keep up, annotating it with a Union of the (growing) list of admissible types, but eventually the list will become silly and the function will earn a # pyre-ignore annotation. This defeats the whole point of the pointless exercise.
So, if the jit compiler needs the annotation I am happy to provide it, but otherwise I will proactively not provide any, and I will sometimes even delete existing annotations when they are devolving into silliness.
- detaro 1y agoCan't you define your own hint for "type that has __getitem__ taking int"?
- crabbone 1y agoThe way I understand parent is that such a type would be too broad. The bigger problem is that the type system expressed through hints in Python is not the type system Python is actually using. It's not even an approximation. You can express in the hint type system things that are nonsense in Python and write Python that is nonsense in the type system implied by hints. The type system introduced through typing package and the hints is a tribute to the stupid fashion. But, also, there is no syntax and no formal definitions to describe Python's actual type system. Nor do I think it's a very good system, not to the point that it would be useful to formalize and study. In Russian, there's an expression "like a saddle on a cow", I'm not sure what the equivalent in English would be. This describes a situation where someone is desperately trying to add a desirable feature to an exiting product that ultimately is not compatible with such a feature. This, in my mind, is the best description of the relationship between Python's actual type system and the one from typing package.
- bc569a80a344f9c 1y ago> In Russian, there's an expression "like a saddle on a cow", I'm not sure what the equivalent in English would be “To fit a square peg into a round hole”
- crabbone 1y agoClose but not the same. In Russian, the expression implies an "upgrade", a failed attempt at improving something that either doesn't require improvement or cannot be improved in this particular way. This would be a typical example of how it's used: "I'm going to be a welder, I need this bachelor's degree like a saddle on a cow!".
- throw-the-towel 1y ago"Lipstick on a pig"? Although that's quite more combative than the Russian phrase.
- crabbone 1y agoYeah... this seems like it would fit the bill nicely. At least, this is the way I'd translate it if I had to. Just didn't think about it.
- thr0w4w4y1337 1y agofrom typing import Protocol, TypeVar T_co = TypeVar("T_co", covariant=True) class Indexable(Protocol[T_co]): def __getitem__(self, i: int) -> T_co: ... def f(x: Indexable[str]) -> None: print(x[0]) I am failing to format it proprely here, but you get the idea.
- matusp 1y agoThere is also bunch of prepackaged types, such as collections.abc.Sequence that could be used in this case.
- thr0w4w4y1337 1y agoSequence does not cut it, since the op mentioned int indexed dictionaries. But yeah.
- sevensor 1y agoSequence[SupportsFloat] | Mapping[int,SupportsFloat] Whether or not you explicitly write out the type, I find that functions with this sort of signature often end up with code that checks the type of the arguments at runtime anyway. This is expensive and kind of pointless. Beware of bogus polymorphism. You might as well write two functions a lot of the time. In fact, the type system may be gently prodding you to ask yourself just what you think you’re up to here.
- dragonwriter 1y ago> Sequence[SupportsFloat] | Mapping[int,SupportsFloat] This is really just the same mistake as the original expanding union, but with overly narrow abstract types instead of overly narrow concrete types. If it relies on “we can use indexing with an int and get out something whose type we don’t care about”, then its a Protocol with the following method: def __getitem__(self, i: int, /) -> Any: ... More generally, even if there is a specific output type when indexing, or the output type of indexing can vary but in a way that impacts the output or other input types of the function, it is a protocol with a type parameter T and this method: def __getitem__(self, i: int, /) -> T: ... It doesn’t need to be union of all possible concrete and/or abstract types that happen to satisfy that protocol, because it can be expressed succinctly and accurately in a single Protocol.
- unethical_ban 1y agoIt sounds like that function is rightfully eligible to be ignored or to use the Any designation. To me that's why the system is handy. For functions that have specific inputs and outputs, it helps developers keep things straight and document code. For broad things, write Any or skip it.
- 9dev 1y agoThat's the same complaints people had about TypeScript in the beginning, when libraries such as Express used to accept a wide range of input options that would be a pain to express in types properly. If you look at where the ecosystem is now, though, you'll see proper type stubs, and most libraries get written in TS in the first place anyway. When editing TS code, you get auto-completion out of the box, even for deeply nested properties or conditional types. You can rely on types being what the compiler says they are, and runtime errors are a rarity now (in properly maintained code bases). > The reason is that they are not really a part of the language, they violate the spirit of the language, and in high-usage parts of code they quickly become a complete mess. I'll admit that this is what I hate Python, and it's probably this spirit of the language as you call it. I never really know what parameters a function takes. Library documentation often shows a few use cases, but doesn't really provide a reference; so I end up having to dig into the source code to figure it out on my own. Untyped and undocumented kwargs? Everywhere. I don't understand how someone could embrace so much flexibility that it becomes entirely undiscoverable for anyone but maintainers.
- zelphirkalt 1y agoBecause the flexibility has been a boon and not a problem. The problem only comes when you try to express everything in the type system, that is third party (the type checkers for it) and added on top.
- fn-mote 1y ago> Because the flexibility has been a boon and not a problem Well, you could say that the problem in this case was the lack of documentation, if you wanted. The type signature could be part of the documentation, from this point of view. Let me give a kind-of-concrete example: one year I was working through a fast.ai course. They have a Python layer above the raw ML stuff. At the time, the library documentation was mediocre: the code worked, there were examples, and the course explained what was covered in the course. There were no type hints. It's free (gratis), I'm not complaining. However, once I tried making my own things, I constantly ran into questions about "can this function do X" and it was really hard to figure out whether my earlier code was wrong or whether the function was never intended to work with the X situation. In my case, type hints would have cleared up most of the problems.
- kketch 1y agoThey don’t violate the spirit of the language. They are optional. They don’t change the behaviour at runtime. Type annotations can seem pointless indeed if you are unwilling to learn how to use them properly. Using a giant union to type your (generic) function is indeed silly, you just have to make that function generic as explained in another comment or I guess remove the type hints
- pansa2 1y ago> They don’t violate the spirit of the language. They are optional. That in itself violates the spirit of the language, IMO. “There should be one obvious way to do it”.
- graynk 1y agothe whole language violates this principle tbh, so it's very in spirit
- tdeck 1y agoYeah that ship sailed some time before they added a third way to do templated string interpolation.
- kketch 1y agoWell, precisely: - There is one obvious way to provide type hints for your code, it’s to use the typing module provided by the language which also provides syntax support for it. - You don’t have to use it because not all code has to be typed - You can use formatted strings, but you don’t have to - You can use comprehensions but you don’t have to - You can use async io, but you don’t have to. But it’s the one obvious way to do it in python The obvious way to annotate a generic function isn’t with a giant Union, it’s with duck typing using a Protocol + TypeVar. Once you known that, the obvious way is… pretty obvious. The obvious way not be bothered with type hints because you don’t like them is not to use them! Python is full of optional stuff, dataclasses, named tuples, meta programming, multiple ancestor inheritance. You dont have to use these features, but there are only one way to use them
- j1elo 1y agoNo idea about Python type system, but doesn't it have anything like this? interface IntIndexable { [key: number]: any }
- sdeframond 1y agoIt does! You can specify a protocol like this: class IntIndexable(Protocol[T]): def __getitem__(self, index: int, /) -> T: ... (Edit: formatting)
- deleted 1y ago[deleted]
- j1elo 1y agoThe syntax is definitely harder to grasp but if the mechanism is there, I guess the parent poster's concern can be solved like that. Although I understant that it might have been just a simplified example. Usually the "Real World" can get very complex.
- sdeframond 1y ago> The syntax is definitely harder to grasp Yes it is. I believe the reason is that this is all valid python while typescript is not valid javascript. Also, python's type annotations are available at runtime (eg. for introspection) while typescript types aren't. That said, typescript static type system is clearly both more ergonomic and more powerful than Python's.
- tdeck 1y agoI like Python a lot, and have been using it for personal projects since about 2010. It was only once I started working and encountering long-lived unfamiliar Python codebases regularly that I understood the benefits of type hints. It's not fun to have to trace through 5 or 6 different functions to try to figure out what type is being passed in or returned from something. It's even less fun to find out that someone made a mistake and it's actually two different incompatible things depending on the execution path. That era of Python codebases were miserable to work in, and often ended up in the poorly though out "we don't know how this works and it has too many bugs, let's just rewrite it" category.
- rlpb 1y ago> It's not fun to have to trace through 5 or 6 different functions to try to figure out what type is being passed in or returned from something. My position is that what is intended must be made clear between type hints and the docstring. Skipping this makes for difficult to read code and has no place in a professional setting in any non-trivial codebase. This doesn't require type hints to achieve. :param and :rtype in the docstring are fine if type hints aren't present, or for complex cases, plain English in the docstring is usually better.
- tdeck 1y agoI agree, although I've found that correct and comprehensive use of the doctoring for this purpose has not existed in the environments I've worked in, or the open source codebases I have needed to understand. Something about type hinting makes people more likely to do it.
- Balinares 1y ago:param and :rtype are type hints, just type hints that cannot be validated by tooling and are guaranteed to go out of sync with the code eventually. Proper type hints are typically very easy to add if the codebase is not a mess that passes things around far and wide with no validation. If it is, the problem is not with the type hints.
- boltzmann64 1y ago
- mhitza 1y agoSounds like the ecosystem needs an "indexable" type annotation. Make it an "indexable<int>" for good measure.
- theptip 1y agoRight, this was my thought. Can’t you just use a typing.Protocol on __getitem__ here? https://typing.python.org/en/latest/spec/protocol.html https://typing.python.org/en/latest/spec/protocol.html Something like from typing import Protocol class Indexable(Protocol): def __getitem__(self, i: int) -> Self: ... Though maybe numpy slicing needs a bit more work to support
- nbadg 1y agoIndeed. IMO, the trick to really enjoying python typing is to understand it on its own terms and really get comfortable with generics and protocols. That being said, especially for library developers, the not-yet-existant intersection type [1] can prove particularly frustrating. For example, a very frequent pattern for me is writing a decorator that adds an attribute to a function or class, and then returns the original function or class. This is impossible to type hint correctly, and as a result, anywhere I need to access the attribute I end up writing a separate "intersectable" class and writing either a typeguard or calling cast to temporarily transform the decorated object to the intersectable type. Also, the second you start to try and implement a library that uses runtime types, you've come to the part of the map where someone should have written HERE BE DRAGONS in big scary letters. So there's that too. So it's not without its rough edges, and protocols and overloads can be a bit verbose, but by and large once you really learn it and get used to it, I personally find that even just the value of the annotations as documentation is useful enough to justify the added work adding them. [1] https://github.com/python/typing/issues/213 https://github.com/python/typing/issues/213
- pepoluan 1y agoSlicing is totally hintable as well. Change the declaration to: def __getitem__(self, i: int | slice) Though to be honest I am more concerned about that function that accepts a wild variety of objects that seem to be from different domains... I'd guess inside the function is a HUGE ladder of 'if isinstance()' to handle the various types and special processing needed. Which is totally reeking of code smell.
- TZubiri 1y agoSo the type is anything that implements the index function ([], or __getitem__), I thnink that's a Sequence, similar to Iterable. >from typing import Sequence >def third(something: Sequence): > return indexable[3] however if all you are doing is just iterate over the thing, what you actually need is an Iterable >from typing import Iterable >def average(something:Iterable): > for thing in something: > ... Statistically, the odds of a language being wrong, are much lower than the programmer being wrong. Not to say that there aren't valid critiques of python, but we must think of the creators of programming languages and their creations as the top of the field. If a 1400 chess elo player criticizes Magnus Carlsen's chess theory, it's more likely that the player is missing some theory rather than he found a hole in Carlsen's game, the player is better served by approaching a problem with the mentality that he is the problem, rather than the master.
- pansa2 1y ago> we must think of the creators of programming languages and their creations as the top of the field The people at the top of the type-system-design field aren’t working on Python.
- bgwalter 1y agoThat is sort of ironic because the Pythonistas did not leave out any opportunity to criticize Java. Java was developed by world class experts like Gosling and attracted other type experts like Philip Wadler. No world class expert is going to contribute to Python after 2020 anyway, since the slanderous and libelous behavior of the Steering Council and the selective curation of allowed information on PSF infrastructure makes the professional and reputational risk too high. Apart from the fact that Python is not an interesting language for language experts. Google and Microsoft have already shut down several failed projects.
- TZubiri 1y ago>"Guido: Java is a decent language," 1999 I get the idea that Python and Java went in opposite directions. But I'm not aware of any fight between both languages. I don't think that's a thing either. Regarding stuff that happens in the 2020. Python was developed in the 90s, python 3 was launched in 2008. Besides some notable PEPs like type hints, WSGI, the rest of development are footnotes. The same goes for most languages (with perhaps the exception of the evergrowing C++), languages make strong bc guarantees and so the bulk of their innovation comes from the early years. Whatever occurs in the 20th and 30th year of development is unlikely to be revolutionary or very significant. Especially ignoreable is the drama that might emerge in these discussions, slander, libel inter-language criticism? Just mute that out. I've read some news about some communities like Ruby on Rails or Nix that become overtaken by people and discussions of political nature rather than development, they can just be ignored I think.
- coldtea 1y ago>you better believe that every single admissible type will eventually be fed to this function That's your problem right there. Why are random callers sending whatever different input types to that function? That said, there are a few existing ways to define that property as a type, why not a protocol type "Indexable"?
- knome 1y ago>why not a protocol type it was a sin that python's type system was initially released as a nominal type system. they should have been the target from day one. being unable to just say "this takes anything that you can call .hello() and .world() on" was ridiculous, as that was part of the ethos of the dynamically typed python ecosystem. typechecking was generally frowned upon, with the idea that you should accept anything that fit the shape the receiving code required. it allowed you to trivially create resource wrappers and change behaviors by providing alternate objects to existing mechanisms. if you wanted to provide a fake file that read from memory instead of an actual file, it was simple and correct. the lack of protocols made hell of these patterns for years.
- marcosdumay 1y agoAFAIK, Python is missing a fully-featured up to date centralized documentation on how to use type annotations. The current docs are "Microsoft-like", they have everything, spread through different pages, in different hierarchies, some of them wrong, and with nothing telling you what else exists.
- jkrejcha 1y agoI disagree. I think, if the decision was made today, it probably would have ended up being structural, but the fact that it isn't enables (but doesn't necessarily force) Python to be more correct than if it weren't (whereas forced structural typing has a certain ceiling of correctness). Really it enabled the Python type system to work as well as it does, as opposed to TypeScript, where soundness is completely thrown out except for some things such as enums Nominal typing enables you to write `def ft_to_m(x: Feet) -> Meters: and be relatively confident that you're going to get Feet as input and Meters as output (and if not, the caller who ignored your type annotations is okay with the broken pieces). The use for protocols in Python in general I've found in practice to be limited (the biggest usefulness of them come from the iterable types), when dealing with code that's in a transitional period, or for better type annotations on callables (for example kwargs, etc).
- deleted 1y ago[deleted]
- zenkey 1y agoIn my experience, the right tooling makes Python typing a big win. Modern IDEs give comprehensive real-time feedback on type errors, which is a big productivity boost and helps catch subtle bugs early (still nowhere near Rust, but valuable nonetheless). Push it too far though, and you end up with monsters like Callable[[Callable[P, Awaitable[T]]], TaskFunction[P, T]]. The art is knowing when to sprinkle types just enough to add clarity without clutter.
- ddavis 1y agoWhen you hit types like that type aliases come to the rescue; a type alias combined with a good docstring where the alias is used goes a long way
- eloisius 1y agoOn the far end of this debate you end up with types like _RelationshipJoinConditionArgument which I'd argue is almost more useless than no typing at all. Some people claim it makes their IDE work better, but I don't use an IDE and I don't like the idea of doing extra work to make the tool happy. The opposite should be true. sqlalchemy.orm.relationship(argument: _RelationshipArgumentType[Any] | None = None, secondary: _RelationshipSecondaryArgument | None = None, *, uselist: bool | None = None, collection_class: Type[Collection[Any]] | Callable[[], Collection[Any]] | None = None, primaryjoin: _RelationshipJoinConditionArgument | None = None, secondaryjoin: _RelationshipJoinConditionArgument | None = None, back_populates: str | None = None, order_by: _ORMOrderByArgument = False, backref: ORMBackrefArgument | None = None, overlaps: str | None = None, post_update: bool = False, cascade: str = 'save-update, merge', viewonly: bool = False, init: _NoArg | bool = _NoArg.NO_ARG, repr: _NoArg | bool = _NoArg.NO_ARG, default: _NoArg | _T = _NoArg.NO_ARG, default_factory: _NoArg | Callable[[], _T] = _NoArg.NO_ARG, compare: _NoArg | bool = _NoArg.NO_ARG, kw_only: _NoArg | bool = _NoArg.NO_ARG, lazy: _LazyLoadArgumentType = 'select', passive_deletes: Literal['all'] | bool = False, passive_updates: bool = True, active_history: bool = False, enable_typechecks: bool = True, foreign_keys: _ORMColCollectionArgument | None = None, remote_side: _ORMColCollectionArgument | None = None, join_depth: int | None = None, comparator_factory: Type[RelationshipProperty.Comparator[Any]] | None = None, single_parent: bool = False, innerjoin: bool = False, distinct_target_key: bool | None = None, load_on_pending: bool = False, query_class: Type[Query[Any]] | None = None, info: _InfoType | None = None, omit_join: Literal[None, False] = None, sync_backref: bool | None = None, **kw: Any) → Relationship[Any]
- sevensor 1y agoI don’t know anything about your jit compiler, but generally the value I get from type annotations has nothing to do with what they do at runtime. People get so confused about Python’s type annotations because they resemble type declarations in languages like C++ or Java. For the latter, types tell the compiler how to look up fields on, and methods that apply to, an object. Python is fine without that. Python’s types are machine-checkable constraints on the behavior of your code.. Failing the type checker isn’t fatal, it just means you couldn’t express what you were doing in terms it could understand. Although this might mean you need to reconsider your decisions, it could just as well mean you’re doing something perfectly legitimate and the type checker doesn’t understand it. Poke a hole in the type checker using Any and go on with your day. To your example, there are several ways described in comments by me and others to write a succinct annotation, and this will catch cases where somebody tries to use a dict keyed with strings or something. Anyway, you don’t have to burn a lot of mental energy on them, they cost next to nothing at runtime, they help document your function signatures, and they help flag inconsistent assumptions in your codebase even if they’re not airtight. What’s not to like?
- deleted 1y ago[deleted]
- gostsamo 1y agowhy not a protocol with getitem with an int arg?
- oivey 1y agoI feel pretty similarly on this. Python’s bolted on type system is very poor at encoding safe invariants common in the language. It’s a straight jacketed, Java-style OOP type system that’s a poor fit for many common Python patterns. I would love it if it were better designed. It’s a real downer that you can’t check lots of Pythonic, concise code using it.
- humdaanm 1y agothe issue of having multiple inputs able to be indexable by ints, is exactly why i prefer that type hints remain exactly as "hints" and not as mandated checks. my philosophy for type hints is that they are meant to make codebases easier to understand without getting into a debugger. their functional equivalence should be that of comments. it's a cleaner more concise way of describing a variable instead of using a full on docstring. though maybe there's a path forward to give a variable a sort of "de-hint" in that in can be everything BUT this type(i.e. an argument can be any indexable type, except a string)
- true_blue 1y ago>though maybe there's a path forward to give a variable a sort of "de-hint" in that in can be everything BUT this type I think this is called a negation type, and it acts like a logical NOT operator. I'd like it too, and I hear that it works well with union types (logical OR) and intersection types (logical AND) for specifying types precisely in a readable way.
- lucasyvas 1y agoThis is like saying you don’t like nails because you don’t understand how to use a hammer though. Developers are not understanding how to use the hints properly which is causing you a personal headache. The hints aren’t bad, the programmers are untrained - the acknowledgement of this is the first step into a saner world.
- jonas21 1y ago> eventually the list will become silly and the function will earn a # pyre-ignore annotation. This defeats the whole point of the pointless exercise. No, this is the great thing about gradual typing! You can use it to catch errors and provide IDE assistance in the 90% of cases where things have well-defined types, and then turn it off in the remaining 10% where it gets in the way.
- bartread 1y ago> The reason is that they are not really a part of the language, they violate the spirit of the language This is a good way of expressing my own frustration with bolting strong typing on languages that were never designed to have it. I hate that TypeScript has won out over JavaScript because of this - it’s ugly, clumsy, and boilerplatey - and I’d be even more disappointed to see the same thing happen to the likes of Python and Ruby. My background is in strongly typed languages - first C++, then Java, and C# - so I don’t hate them or anything, but nowadays I’ve come to prefer languages that are more sparing and expressive with their syntax.
- burnerRhodov2 1y agoYou explained some hyper niche instance where type hints should be ignored. 99% of the time, they are extremely helpful.
- chlorion 1y agoIt's not even a niche instance, protocols solve their problem lol.
- fyrn_ 1y agoYou can use a Protocol type for that, makes a lot mote sense than nominal typing for typing use case.
- vjerancrnjak 1y agoExactly, sounds like misuse of unions. Although Python type hints are not expressive enough.
- deleted 1y ago[deleted]
- the__alchemist 1y agoI like your point! I think the advantage in its light is this: People often use Python because it's convention in the domain, the project already uses it, or it's the language the rest of the team uses. So, you are perhaps violating the spirit, but that's OK. You are making the most of tools available. It's not the Platonic (Pythonic??) ideal, but good enough.
- bobbiechen 1y agoIsn't this supported by typing.SupportsIndex? https://docs.python.org/3/library/typing.html#typing.SupportsIndex https://docs.python.org/3/library/typing.html#typing.Support... Mind you, I haven't used it before, but it feels very similar to the abstract Mapping types.
- detaro 1y ago__index__ is not what you think it is
- bobbiechen 1y agoOops, thanks for the correction. That's on me for drive-by commenting.
- thrance 1y agoFor this very specific example, isn't there something like "Indexable[int]"?
- nurettin 1y agoWhy not do Indexable = Any and pass that? Even if it doesn't help your jit or the IDE, at least it is more explicit than def lol(blarg): # types? haha you wish. rtfc you poor sod. Pytharn spirit ftw!!! ... return omg[0].wtf["lol freedom"].pwned(Good.LUCK).figuring * out
- tickettotranai 1y agoI mean, you can just... Not annotate something if creating the relevant type is a pain. Static analysis \= type hints, and even then... Besides, there must be some behavior you expect from this object. You could make a type that reflects this: IntIndexable or something, with an int index method and whatever else you need. This feels like an extremely weak argument. Just think of it as self-enforcing documentation that also benefits auto-complete; what's not to love? Having an IntIndexable type seems like a great idea in your use case.
- rtpg 1y ago> For example a common failure mode in my work’s codebase is that some function will take something that is indexable by ints. The type could be anything, it could be List, Tuple, Dict[int, Any], torch.Size, torch.Tensor, nn.Sequential, np.ndarray, or a huge host of custom types! And you better believe that every single admissible type will eventually be fed to this function. Sometimes people will try to keep up, annotating it with a Union of the (growing) list of admissible types, but eventually the list will become silly and the function will earn a # pyre-ignore annotation. This defeats the whole point of the pointless exercise. You are looking for protocols. A bit futzy to write once but for a heavily trafficked function it's woth it. If your JIT compiler doesn't work well with protocols... sounds like a JIT problem not a Python typing problem
- chlorion 1y ago>The type could be anything, it could be List, Tuple, Dict[int, Any], torch.Size, torch.Tensor, nn.Sequential, np.ndarray, or a huge host of custom types! That's not how you are supposed to use static typing? Python has "protocols" that allows for structural type checking which is intended for this exact problem.
- spicyusername 1y agoAnd you better believe that every single admissible type This is exactly why I hate using Python.
- albrewer 1y agoDefine a protocol[0] that declares it implements `__getitem__` and type annotate with that protocol. Whatever properties are needed inside the function can be described in other protocols. These are similar to interfaces in C# or traits in Rust - you describe what the parameter _does_ instead of what it _is_. [0]: https://typing.python.org/en/latest/spec/protocol.html https://typing.python.org/en/latest/spec/protocol.html
- fragmede 1y ago> something that is indexable by ints. > ...Dict[int, Any]... If that is exactly what you want, then define a Protocol: from __future__ import annotations from typing import Protocol, TypeVar T = TypeVar("T") K = TypeVar("K") class GetItem(Protocol[K, T]): def __getitem__(self, key: K, /) -> T: ... def first(xs: GetItem[int, T]) -> T: return xs[0] Then you can call "first" with a list or a tuple or a numpy array, but it will fail if you give it a dict. There is also collections.abc.Sequence, which is a type that has .__getitem__(int), .__getitem__(slice), .__len__ and is iterable. There are a couple of other useful ones in collections.abc as well, including Mapping (which you can use to do Mapping[int, t], which may be of interest to you), Reversible, Callable, Sized, and Iterable.