What do typing.get_origin and typing.get_args return for dict[str, int]?
answer
- Take a subscripted annotation apart at runtime
- One helper names the container
- The other returns a tuple of parameters
- A bare class was never subscripted
- dict[str, int] splits into dict and (str, int)
basics
~20 styping.get_origin(dict[str, int]) returns the plain dict class and typing.get_args returns the tuple (str, int). Together they split a subscripted annotation into its container and its type arguments. For an unsubscripted class such as int, get_origin returns None.
solid answer
~40 sSubscripting a builtin container produces an alias object, and the two `typing` helpers take it apart: `get_origin(dict[str, int])` gives back the runtime class `dict`, and `get_args(dict[str, int])` gives the tuple `(str, int)` in written order. The same pair works on `list[int]` (origin `list`, args `(int,)`), on `collections.abc.Sequence[int]`, and on the legacy `typing.List[int]` spelling, which normalizes to origin `list` — that normalization is the main reason to call the helpers instead of poking at alias attributes yourself. An unsubscripted class is not an alias, so `get_origin(int)` is `None` and `get_args(int)` is `()`; runtime code that walks annotations uses exactly that to tell a scalar from a container. `get_args` does not recurse: for `dict[str, list[int]]` it returns `(str, list[int])`, and the nested alias is yours to unpack with another call.
code
pycon · 9 lines>>> from typing import get_args, get_origin
>>> get_origin(dict[str, int])
<class 'dict'>
>>> get_args(dict[str, int])
(<class 'str'>, <class 'int'>)
>>> get_origin(dict) is None
True
>>> get_args(dict)
()go deeper
Be ready to say, without hesitating, that one helper gives the container class and the other gives a tuple of the type arguments, and that an unsubscripted class yields None and an empty tuple.
Explain that subscripting builds an alias object holding an origin and an argument tuple, that the helpers normalize the legacy typing spellings to the runtime class, and that the argument tuple is one level deep.
Show the dispatch loop you would actually write: branch on the origin, recurse over the arguments, and decide deliberately what an unparameterized container or an unknown origin does rather than letting it crash on user data.
Own the question of whether runtime type introspection belongs in your codebase at all — where a declaration-driven converter pays for itself, and where a hand-written parse function is cheaper to read and to keep correct.
### What a subscripted annotation actually is When you write `dict[str, int]`, Python does not create a new class. It calls the container class's subscription hook and gets back an *alias object* — for a builtin container, an instance of `types.GenericAlias`. That object remembers two things: the class that was subscripted (the **origin**) and the tuple of things it was subscripted with (the **arguments**). An annotation is just an ordinary expression evaluated to an ordinary object, so at runtime `dict[str, int]` is a value you can pass around, store in a registry, and inspect. `typing.get_origin` and `typing.get_args` are the supported way to inspect it. `get_origin` answers "what container is this?" and `get_args` answers "what was it parameterized with?": ```python from typing import get_args, get_origin get_origin(dict[str, int]) # <class 'dict'> get_args(dict[str, int]) # (<class 'str'>, <class 'int'>) get_origin(list[int]) # <class 'list'> get_args(list[int]) # (<class 'int'>,) ``` Note the shapes: the origin is a **class**, not another alias, and the arguments are always a **tuple**, even when there is exactly one — `(int,)`, with the trailing comma. Code that expects a list, or that forgets the one-element case, breaks immediately. ### Why the helpers exist rather than direct attribute access Alias objects do expose their origin and argument tuple as attributes, and you could read them. You should not, for two reasons. First, Python has several alias flavours that all mean the same thing: `list[int]` is a `types.GenericAlias`, the older `typing.List[int]` is a typing-internal alias, and `collections.abc.Sequence[int]` is a third. `get_origin` normalizes across all of them — `get_origin(typing.List[int])` is the runtime class `list`, which is what you want if you are about to construct something. Second, the helpers degrade gracefully on things that are not aliases at all, returning `None` and `()` rather than raising `AttributeError`, so a dispatch function can lead with them. ### The unsubscripted case is the branch that matters `get_origin(int)` is `None`, `get_origin(str)` is `None`, and `get_origin(list)` — the bare container class, with no subscription — is also `None`, with `get_args(list) == ()`. That is not a defect; a bare `list` in an annotation genuinely carries no element information. Runtime code that builds values from declared shapes almost always uses `origin is None` as its "this is a plain class, just call it" branch: ```python def build(ann, raw): origin = get_origin(ann) if origin is None: return ann(raw) # int('42'), Decimal('1200.50') if origin is list: (item,) = get_args(ann) return [build(item, part) for part in raw.split(';')] raise TypeError(f'unsupported annotation: {ann!r}') ``` That shape — check the origin, then recurse over the arguments — is the whole pattern, and it is why the two functions are almost always used together. ### Arguments are shallow, and order is written order `get_args` returns one level. `get_args(dict[str, list[int]])` is `(str, list[int])`; the second element is still an alias, and getting `int` out of it takes another `get_origin`/`get_args` pair. Recursion is the caller's job. Order is the order you wrote: for a mapping that means key type first, value type second, so `dict[str, int]` and `dict[int, str]` are distinguishable, as they must be. ### Where the sharp edges are A few results surprise people. Subscripted `collections.abc.Callable` reports origin `collections.abc.Callable` and returns its parameter list as the first argument and the return type as the last. `typing.Literal['a', 'b']` reports origin `typing.Literal` and returns the *values*, not types, as its arguments — `get_args` is not promised to hand you classes. And a union reports a union origin rather than a container class, which is its own topic. ### Versions `get_origin` and `get_args` arrived in 3.8. Subscripting builtins directly — `list[int]`, `dict[str, int]` — is 3.9 and later (PEP 585); before that you needed `typing.List[int]`, which still works and still normalizes to origin `list`. Everything described here holds on 3.14.
- What does typing.get_args return for a bare list used as an annotation, and why does that matter?It returns the empty tuple, and `get_origin(list)` returns `None`, because nothing was subscripted. Code that walks annotations has to decide what an unparameterized container means: either refuse it, or fall back to treating the elements as `object` and leaving them unconverted. Silently unpacking `get_args(...)[0]` there raises `IndexError` on the first bare container it meets.
- Does typing.get_origin(typing.List[int]) differ from typing.get_origin(list[int])?No — both return the runtime class `list`, and `get_args` returns `(int,)` for both. The two aliases are different objects internally (`list[int]` is a `types.GenericAlias`, `typing.List[int]` is a typing-internal alias), but `get_origin` normalizes them, which is exactly why you call the helper instead of reading alias attributes yourself.
saying these in an interview costs you the question
- Says get_origin(dict[str, int]) returns dict[str, int] itself
- Expects get_args to return a list rather than a tuple
- Thinks get_origin(int) raises instead of returning None
- Assumes get_args recurses into nested arguments automatically
- Believes the helpers only work on typing aliases, not builtins