You can specify recursive types in the typing language by using type aliases and forward reference strings,

Garthoks = Union[Garthok, Iterable['Garthoks']]

Mypy supports recursive types by default since v0.990, and Pyright/Pylance since v2020.9.4.

Some types of forward references are handled by PEP 563. You can use them starting from Python 3.7 by doing from __future__ import annotations – Konstantin


As of Python 3.12, __future__.annotations/stringifying is not necessary if the type is defined using a type statement:

type Garthoks = Garthok | Iterable[Garthoks]
Answer from gilch on Stack Overflow
🌐
Turingtaco
turingtaco.com › recursive-types
Recursive Types in Python - The Turing Taco Tales
December 7, 2024 - Mutually recursive types are declared in Python with forward references, which are strings, and Self Types simplify the Self Recursive case.
Discussions

Generic `typing.ForwardRef` to support generic recursive types - Ideas - Discussions on Python.org
Since v0.981 mypy supports recursive types and will be enabled by default since v0.990. E.g.: JSON = Union[Dict[str, 'JSON'], List['JSON'], str, int, float, bool, None] Recursive types need to use ForwardRefs at the right-hand side to reference the type alias before assignment. More on discuss.python.org
🌐 discuss.python.org
2
October 21, 2022
Recursive Generic Type Support
Bug Report In: #731 #13297 (not released yet in v0.971) We now support recursive type hints, such as: JSON = Union[Dict[str, 'JSON'], List['JSON'], str, int, float, bool, None] But ... More on github.com
🌐 github.com
1
September 20, 2022
Recursive type hints
You seem to have a bit of a wrong idea about static type checkers. Mypy only tracks types, not values. That means it has no idea whether test[-1] is a list or an int. As far as mypy is concerned, test[-1] is a Union[int, 'NestedList'], and that can't be assigned to a variable of type NestedList. More on reddit.com
🌐 r/learnpython
5
3
February 25, 2024
Recursive Generic Type Hints (python 3.12)
type _RList[U] = list[int | _RList[U]] What's the point of U here? More on reddit.com
🌐 r/Python
9
29
April 10, 2025
🌐
Python.org
discuss.python.org › typing
Recursive type annotation for a nested list of lists of lists of - Typing - Discussions on Python.org
December 11, 2024 - I’m trying to figure out the way to write type annotations to represent a type nested[T,N] such that: nested[T,0] = list[T] nested[T,1] = list[list[T]] nested[T,2] = list[list[list[T]]] etc Is there a good way of doing this? I have hundreds of functions with signatures mostly being like: def func1(p: nested[T,0]) -> T: ... def func2(p: nested[T,0], q: nested[T,0]) -> nested[T,0]: ... def func3(p: nested[T,N], q: nested[T,N], N: int) -> nested[T,N]: ... def func4(p: nested[T,N], ...
🌐
MIT
web.mit.edu › 6.005 › www › fa14 › classes › 11-recursive-data-types
Reading 11: Recursive Data Types
In the diagram, we can see how the stack grows as `main` calls `factorial` and `factorial` then calls *itself*, until `factorial(0)` does not make a recursive call. Then the call stack unwinds, each call to `factorial` returning its answer to the caller (where it is assigned to `r`), until `factorial(3)` returns to `main` (where the answer is assigned to `x`). [Here's an **interactive visualization for a Python version of `factorial`**](http://www.pythontutor.com/visualize.html#code=def+factorial(n): ++++if+n+==+0: ++++++++return+1 ++++else: ++++++++r+=+factorial(n-1) ++++++++return+n+*+r x+=+factorial(3) &mode=display&origin=opt-frontend.js&cumulative=false&heapPrimitives=false&drawParentPointers=false&textReferences=false&showOnlyOutputs=false&py=2&rawInputLstJSON=[]&curInstr=1).
🌐
Python.org
discuss.python.org › ideas
Generic `typing.ForwardRef` to support generic recursive types - Ideas - Discussions on Python.org
October 21, 2022 - E.g.: JSON = Union[Dict[str, 'JSON'], List['JSON'], str, int, float, bool, None] Recursive types need to use ForwardRefs at the right-hand side to reference the type alias before assignment.
🌐
GitHub
github.com › python › mypy › issues › 13693
Recursive Generic Type Support · Issue #13693 · python/mypy
September 20, 2022 - Bug Report In: #731 #13297 (not released yet in v0.971) We now support recursive type hints, such as: JSON = Union[Dict[str, 'JSON'], List['JSON'], str, int, float, bool, None] But ...
Author: python
🌐
Reddit
reddit.com › r/learnpython › recursive type hints
r/learnpython on Reddit: Recursive type hints
February 25, 2024 -

I have lists within lists with integer items (or bytes, but for example purposes ints are enough), for example: [1, 2, [3, 4], 5]

How can I go about creating a type hint for this structure?

This is what I've made:

from typing import List, Union

NestedList = List[Union[int, 'NestedList']]

test: NestedList = [1, 2, [3], 4]
test.append([5])
test = test[-1]

But mypy fails with:

test_recursive_hints.py:7: error: Incompatible types in 
assignment (expression has type "int | NestedList", 
variable has type "NestedList")  [assignment]

How to solve this problem?

Find elsewhere
🌐
QuantInsti
blog.quantinsti.com › recursive-functions-python
Recursive Functions in Python: Concepts, Types, and Applications in Trading
June 27, 2024 - In Python, recursive functions can be categorised into different types based on their structure and how they make recursive calls.⁽²⁾
🌐
Reddit
reddit.com › r/python › recursive generic type hints (python 3.12)
r/Python on Reddit: Recursive Generic Type Hints (python 3.12)
April 10, 2025 -

TIL from this video typing a recursive flatten (by YT channel anthonywritescode) that you can now type hint recursive data & functions with generic type parameter!

# new syntax
# recursive type for nested list having elems of same type (eg. int)
type _RList[U] = list[U | _RList[U]]

def flatten[T](lst: _RList[T]) -> _RList[T]:
     """ Flatten nested list.""""
     return [
         flatten(x) if isinstance(x, list) else x
         for x in lst
     ]

NOTE: Latest mypy type checks this new syntax, but editor / IDE may not recognize it yet.

Did you all know about this? Have you found more such cool type hinting syntax in Python?

🌐
GitHub
github.com › python › mypy › issues › 731
Support recursive types · Issue #731 · python/mypy
July 30, 2015 - The following in particular would be useful: Callback = Callable[[str], 'Callback'] Foo = Union[str, List['Foo']]
Author: python
🌐
Hacker News
news.ycombinator.com › item
Last I checked, mypy struggled to represent recursive types. Think `JSON = Union... | Hacker News
May 11, 2021 - Worse, importing third party packages often fails silently and when you can get error messages they tend to be completely inactionable (I recall one error message which linked to a web page that had lots of details and workarounds for fixing other problems, but none of which solved the error itself)
🌐
ScholarHat
scholarhat.com › home › tutorials › python › python recursion: types o..
Python Recursion: Types of Recursion in Python (Full Guide)
September 11, 2025 - Here are the common forms of recursion in Python: Tail Recursion is a unique type of recursion where the recursive call is the last operation in the function. There is no requirement to hold any previous state after the recursive call, allowing for optimization in some languages.
🌐
Sling Academy
slingacademy.com › article › recursive-types-in-modern-python-a-practical-guide
Recursive Types in Modern Python: A Practical Guide - Sling Academy
The typing module in Python 3.5 introduced support for type hints, which can be particularly helpful when working with recursive structures. To correctly type annotate a recursive class, Python 3.7 introduced the from __future__ import annotations feature, allowing for forward references in type hints.
🌐
Gitbooks
wizardforcel.gitbooks.io › sicp-in-python › content › 18.html
3.3 Recursive Data Structures | SICP in Python - wizardforcel
Recall that the recursive list abstract data type represented a list as a first element and the rest of the list. We previously implemented recursive lists using functions, but at this point we can re-implement them using a class.
🌐
GitHub
github.com › python › typeshed › issues › 7904
Recursive type aliases tracker · Issue #7904 · python/typeshed
May 21, 2022 - from typing import TypeAlias Recursive: TypeAlias = str | list["Recursive"] def foo(r: Recursive) -> None: if not isinstance(r, str): foo(r[0]) if not isinstance(r, list): r.casefold() foo("") foo(list("")) foo(list(list(""), ""))
Author: python
Top answer
1 of 1
1

Assuming that you decide to retain _check_parameters(), here's an illustration of some alternative techniques for organizing a complex boolean check. (1) The isinstance() function will take a tuple of types, so you can do some consolidation. (2) If you need to check for None and check for types, you can do it all in one shot. (3) Some of your checks were repetitive; help the reader by factoring things out. (4) Organize the checks like a pretty-printed data structure because it gets the parens/brackets working for you to convey logical structure. (5) Sometimes simple and fairly banal code comments can function as visual/organizational sign posts to guide the reader. (6) I prefer the lines of code to lead with the substance rather than the boolean operator -- which is mostly a stylistic preference, but I do think it combines better in these kinds of complex checks. For example, I felt no readability-driven urge to add a preliminary True and to the expression. Also, most people use editors with syntax highlighting, so the operators pop out visually and there's no need to waste the prime real estate (the start of each line) on the operator.

TNone = type(None)

check_ctype_seq = lambda ctypes, cls: (
    isinstance(ctypes, cls) and
    all(
        isclass(ct) and
        issubclass(ct, GenericContainer)
        for ct in ctypes
    )
)

return (
    # Length and depth.
    isinstance(length, (int, Callable, TNone)) and
    isinstance(depth, (int, Callable, TNone)) and
    # Matching type.
    (
        isinstance(matching_type, (TNone, type)) or
        is_n_list(matching_type, None, type) or
        is_n_tuple(matching_type, None, type)
    ) and
    # Container_type.
    (
        container_type is None or
        (
            isclass(container_type)
            and issubclass(container_type, GenericContainer)
        ) or
        check_ctype_seq(container_type, list) or
        check_ctype_seq(container_type, tuple)
    )
)
🌐
Python documentation
docs.python.org › 3 › library › typing.html
typing — Support for type hints
1 week ago - >>> type Mutually = Recursive >>> type Recursive = Mutually >>> Mutually Mutually >>> Recursive Recursive >>> Mutually.__value__ Recursive >>> Recursive.__value__ Mutually
Top answer
1 of 3
6

Since Python is dynamically typed, there is no issue defining whatever classes you need.

class Tree:
    left = None
    right = None
    def __init__(self, left, right):
        self.left = left
        self.right = right

Even if you are interested in typing these definitions, you can do that like in any other class-based object oriented language:

from typing import Union

class Tree:
    left: Union['Tree', int]
    right: Union['Tree', int]
    def __init__(self, left: Union['Tree', int], right: Union['Tree', int]) -> None:
        self.left = left
        self.right = right

Note the use of strings for the name of the type (which you can avoid in more recent Python versions).

See this open issue in mypy for direct recursive algebraic types such as

Tree = Union[Tuple['Tree', 'Tree'], int]

The most common (though not necessarily recommended) way of defining the WordTree you describe is using a superclass and a shallow hierarchy:

from typing import List, final

class WordTree: pass

@final
class Word(WordTree):
    word: str

@final
class Subword(WordTree):
    subword: str
    children: List[WordTree]

@final
class Root(WordTree):
    children: List[WordTree]

Using such an implementation might require using isinstance checks (though Python3.10 gives you nice sugar for those). Constructors are omitted in this example to avoid clutter; you might want to use dataclass to get them, and other kinds of behavior, easily.

To date, Python gives you no way to disallow unrelated classes from inheriting from WordTree, thus breaking some of the ability to statically reason about such programs.

Some other OOP languages, such as Scala and Kotlin and (soon) Java, can take such a definition (using sealed classes) and give you type checks and syntactic constructs that are similar to the ones given by functional languages such as Haskell.


For all I know, this kind of design is usually recommended only for pure-data classes, such as ASTs. It is less suited for defining user-facing container such as trie, since it exposes the inner workings of the data structure. So even if you go with that design, you might want to use it as an implementation detail, and use another class, Trie, to be used by client code through a well-defined API. That class can have a WordTree field, or any other way of implementing the same logic.

IMO this is essential to how object-oriented design differs from functional design. The latter focuses on data flow and on static reasoning, whereas the former focuses on APIs, extensibility and decoupling. I think this is helpful to note, when porting between languages and environments - though as noted above, some languages try to enable both design approaches.

2 of 3
4

Here's an equivalent implementation of the Haskell binary tree in Python 3.10. Static type checking can be done with mypy.

from __future__ import annotations
from dataclasses import dataclass
from typing import Generic, TypeVar

T = TypeVar("T")

@dataclass
class Branch(Generic[T]):
    value: T
    left: Tree[T]
    right: Tree[T]

@dataclass
class Leaf(Generic[T]):
    value: T

Tree = Branch[T] | Leaf [T]

You can use it like this (note the pattern-matching in the contains function - a new feature of Python 3.10):

def contains(tree: Tree[T], value: T):
    match tree:
        case Leaf(x):
            return x == value
        case Branch(x, left, right):
            return x == value or contains(left, value) or contains(right, value)

tree = Branch(
    1,
    Branch(2, Leaf(3), Leaf(4)),
    Branch(5, Leaf(6), Branch(4, Leaf(7), Leaf(8)))
)

assert contains(tree, 1)
assert contains(tree, 5)
assert contains(tree, 8)

To implement your WordTree, you would do the following:

from __future__ import annotations
from dataclasses import dataclass

@dataclass
class Word:
    value: str

@dataclass
class Subword:
    value: str
    trees: list[WordTree]

@dataclass
class Root:
    trees: list[WordTree]

WordTree = Word | Subword | Root

A note on the imports:

  • from __future__ import annotations allows you to annotate with the name of a type that hasn't been defined yet.
  • @dataclass automatically defines a constructor for you.