typing_utils

Backports Python 3.8+ typing introspection utilities and adds issubtype() for runtime subtype checks across older Python versions.

Library
PyPI
v0.1.0
12stars
Apache License 2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
27/100Needs Attention
Development Activity0
Maintenance20
Community16
Maturity60
Momentum12

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
52/100Fair
Architecture62
Code Quality45
Innovation55
Learning Curve45

typing-utils backports the typing introspection helpers that Python only shipped starting in version 3.8 — get_origin, get_args, and get_type_hints — so codebases still supporting Python 3.6 and 3.7 can use the same generic-type introspection API without version branching. On top of that backport it adds issubtype(), a function with no equivalent in the standard library that determines whether one type is a structural subtype of another, including nested generics, Union members, and forward references resolved from a supplied namespace.

The library is a single ~440-line module with no runtime dependencies beyond the standard library, making it a lightweight drop-in for type-checking tools, serialization libraries, and validation frameworks that need to reason about typing.List[int] vs typing.Sequence at runtime rather than at static-analysis time.

What You Get

  • issubtype() - Determines if one type is a subtype of another, covering plain classes, generics, Union members, and forward references.
  • get_origin() and get_args() backports - Version-independent implementations that behave identically across Python 3.6 through 3.9, delegating to the stdlib versions when available on 3.8+.
  • get_type_hints() re-export - Direct passthrough of typing.get_type_hints so all four utilities can be imported from one place.
  • NormalizedType representation - A hashable, comparable NamedTuple form of any type expression, used internally and exposed for callers that need to deduplicate or compare type expressions.
  • Forward reference resolution - issubtype() accepts a forward_refs mapping to resolve string-based recursive type aliases such as a self-referential JSON type.

Common Use Cases

  • Runtime validation libraries - A serialization or validation library checks whether a supplied value’s inferred type satisfies a field’s declared typing.Union or typing.Sequence annotation without hand-rolling isinstance() chains.
  • Cross-version typing support - A package that still supports Python 3.6/3.7 needs get_origin()/get_args() behavior identical to Python 3.8+ without maintaining separate code paths per interpreter.
  • Static-analysis tooling on older interpreters - A linter or type-checker plugin running under an older Python interpreter needs to introspect generic types the same way typing.get_origin does on newer ones.
  • Recursive/forward-referenced type checks - A schema library defines a recursive type alias (e.g. a JSON type) as a string forward reference and needs to verify a candidate type is a subtype of it.

Under The Hood

Architecture typing_utils is a single flat module (typing_utils/init.py, ~440 lines) with no internal package layering. It’s organized as constants (BUILTINS_MAPPING, STATIC_SUBTYPE_MAPPING) feeding a normalization layer (_normalize_aliases, normalize, the NormalizedType NamedTuple), which feeds a recursive comparison engine (_is_origin_subtype, _is_origin_subtype_args, _is_normal_subtype), topped by the public API (issubtype, get_origin, get_args, get_type_hints) re-exported via all. Data flow is linear: raw typing expressions pass through normalize() into a hashable NormalizedType before _is_normal_subtype recursively resolves Union, TypeVar, and ForwardRef branches, falling through to origin-plus-args comparison. Any change to NormalizedType’s eq/hash contract — which deliberately overrides frozenset containment semantics for Union args — would silently alter equality behavior for every downstream subtype check.

Tech Stack Pure Python with zero runtime dependencies, using only collections.abc, io, itertools, and typing from the standard library. Packaging is legacy setuptools.setup() in setup.py with no pyproject.toml or modern build backend; pytest is declared as an optional test extra rather than a dev dependency. CI runs as four separate GitHub Actions workflow files (py36.yml through py39.yml) rather than a single version matrix. There is no mypy, linter, or pre-commit configuration despite this being a typing-focused library.

Code Quality A single test file (tests/test_common.py, ~213 lines) exercises normalize() and issubtype() across builtins, generics, unions, and forward references using plain assert statements rather than pytest fixtures or parametrization. Naming is consistent snake_case with underscore-prefixed private helpers. Error handling is minimal, relying on assertions and a bare NotImplementedError for unsupported Python versions rather than custom exception types. No linter, formatter, or type-checker is configured in the repo, which is a notable gap for a library whose entire purpose is typing introspection.

API Design The public surface is deliberately narrow — four functions, three of which mirror stdlib names (get_origin, get_args, get_type_hints) for near drop-in familiarity. issubtype() is the standout addition, returning Optional[bool] (True/False/None) to represent an explicit “cannot be determined” case rather than defaulting to False or raising — a correctness-conscious choice that pushes extra handling onto callers. Each public function carries a runnable-example docstring, but there’s no dedicated documentation site or parameter-level reference beyond the README and those docstrings.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search