I'm trying out Python's type annotations with abstract base classes to write some interfaces. Is there a way to annotate the possible types of *args and **kwargs...How does one annotate the sensible types for *args and **kwargs
There are two general usage categories when it comes to type hinting:
- Writing your own code (which you can edit and change)
- Using 3rd party code (which you can't edit, or is hard to change)
Most users have some combo of both.
The answer depends on whether your *args and **kwargs have homogeneous types (i.e. all of the same type) or heterogenous types (i.e. different types), as well as whether there is a fixed number of them or a variable/indeterminate number of them (the term used here is fixed vs. variable arity)
*args and **kwargs have sometimes been used in what I'm loosely calling a "Python-specific design pattern" (see below). It is important to understand when this is being done because it affects the way you should type hint.
Best practice, always, is to stand on the shoulders of giants:
- I highly recommend reading and studying the
typeshed .pyi stubs, especially for the standard library, to learn how developers have typed these things in the wild.
For those who want to see a HOW-TO come to life, please consider upvoting the following PRs:
Case 1: (Writing Your Own Code)
*args
(a) Operating on a Variable Number of Homogeneous Arguments
The first reason *args is used is to write a function that has to work on a variable (indeterminate) number of homogoeneous arguments
Example: summing numbers, accepting command line arguments, etc.
In these cases, all *args are homogeneous (i.e. all the same type).
Example: In the first case, all arguments are ints or floats; In the second case, all arguments are strs.
It is also possible to use Unions, TypeAliass, Generics, and Protocols as the type for *args.
I claim (without proof) that operating on an indeterminate number of homogeneous arguments was the first reason *args was introduced into the Python language.
Consequently, PEP 484 supports providing *args a homogeneous type.
Note:
Using *args is done much less often than specifying parameters explicitly
(i.e. logically, your code base will have many more functions that don't use *args than do). Using *args for homogeneous types is normally done to avoid requiring users
to put arguments into a
container
before passing them to the function.
It is recommended to type parameters
explicitly wherever
possible.
- If for nothing else, you would normally be documenting each argument with its type in a docstring anyway (not
documenting is a quick way to make others not want to use your code,
including your future self.)
Note also that args is a tuple because the unpacking operator (*) returns a tuple, so note that you can't mutate args directly (You would have to pull the mutable object out of args).
(b) Writing Decorators and Closures
The second place where *args will pop up is in decorators. For this, using ParamSpec as described in PEP 612 is the way to go.
(c) Top-Level Functions that Call Helpers
This is the "Python-specific design pattern" I alluded to. For Python >= 3.11, the python docs show examples where you can use TypeVarTuple to type this so the type information is preserved between calls.
- Using
*args this way is typically done to reduce the amount of code to write, esp. when the arguments between multiple functions are the same
- It has also been used to "swallow up" a variable number of arguments through tuple unpacking that may not be needed in the next function
Here, items in *args have heterogenous types, and possibly a variable number of them, both of which can be problematic.
The Python typing ecosystem does not have a way to specify heterogenous *args. 1
Before the advent of type checking, developers would need to check the type of individual arguments in *args (with assert, isinstance, etc.) if they needed to do something differently depending on the type:
Examples:
- You need to print passed
strs, but sum the passed ints
Thankfully, the mypy developers included type inference and type narrowing to mypy to support these kinds of situations. (Also, existing code bases don't need to change much if they were already using assert, isintance, etc., to determine the types of the items in *args)
Consequently, in this case you would do the following:
- Give
*args the type object so its elements can be any type, and
- use type narrowing where needed with
assert ... is (not) None, isinstance, issubclass, etc., to determine the types of individual items in *args
1 Warning:
For Python >= 3.11, *args can be typed with
TypeVarTuple, but this is meant to be used when type hinting
variadic generics. It should not be used for typing *args in the general
case.
TypeVarTuple was primarily introduced to help type hint numpy
arrays, tensorflow tensors, and similar data structures, but for Python >= 3.11, it can be used to preserve type information between calls for top-level functions calling helpers as stated before.
Functions that process heterogenous *args (not just pass them through) must still type
narrow to
determine the types of individual items.
For Python <3.11, TypeVarTuple can be accessed through
typing_extensions, but to date there is only provisional support for it through pyright (not mypy). Also, PEP 646 includes a section on using *args as a Type Variable
Tuple.
**kwargs
(a) Operating on a Variable Number of Homogeneous Arguments
PEP 484 supports typing all values of the **kwargs dictionary as a homogeneous type. All keys are automatically strs.
Like *args, it is also possible to use Unions, TypeAliass, Generics, and Protocols as the type for *kwargs.
I've not found a compelling use case for processing a homogeneous set of named arguments using **kwargs.
(b) Writing Decorators and Closures
Again, I would point you to ParamSpec as described in PEP 612.
(c) Top-Level Functions that Call Helpers
This is also the "Python-specific design pattern" I alluded to.
For a finite set of heterogeneous keyword types, you can use TypedDict and Unpack if PEP 692 is approved.
However, the same things for *args applies here:
- It is best to explicitly type out your keyword arguments
- If your types are heterogenous and of unknown size, type hint with
object and type narrow in the function body
Case 2: (3rd Party Code)
This ultimately amounts to following the guidelines for the part (c)s in Case 1.
Outtro
Static Type Checkers
The answer to your question also depends on the static type checker you use. To date (and to my knowledge), your choices for static type checker include:
mypy: Python's de facto static type checker
pyright: Microsoft's static type checker
pyre: Facebook/Instagram's static type checker
pytype: Google's static type checker
I personally have only ever used mypy and pyright. For these, the mypy playground and pyright playground are great places to test out type hinting your code.
Interfaces
ABCs, like descriptors and metaclasses, are tools for building frameworks (1). If there's a chance you could be turning your API from a "consenting adults" Python syntax into a "bondage-and-discipline" syntax (to borrow a phrase from Raymond Hettinger), consider YAGNE.
That said (preaching aside), when writing interfaces, it's important to consider whether you should use Protocols or ABCs.
Protocols
In OOP, a protocol is an informal interface, defined only in documentation and not in code (see this review article of Fluent Python, Ch. 11, by Luciano Ramalho). Python adopted this concept from Smalltalk, where a protocol was an interface seen as a set of methods to fulfill. In Python, this is achieved by implementing specific dunder methods, which is described in the Python data model and I touch upon briefly here.
Protocols implement what is called structural subtyping. In this paradigm, _a subtype is determined by its structure, i.e. behavior), as opposed to nominal subtyping (i.e. a subtype is determined by its inheritance tree). Structural subtyping is also called static duck typing, as compared to traditional (dynamic) duck typing. (The term is thanks to Alex Martelli.)
Other classes don't need to subclass to adhere to a protocol: they just need to implement specific dunder methods. With type hinting, PEP 544 in Python 3.8 introduced a way to formalize the protocol concept. Now, you can create a class that inherits from Protocol and define any functions you want in it. So long as another class implements those functions, it's considered to adhere to that Protocol.
ABCs
Abstract base classes complement duck-typing and are helpful when you run into situations like:
class Artist:
def draw(self): ...
class Gunslinger:
def draw(self): ...
class Lottery:
def draw(self): ...
Here, the fact that these classes all implement a draw() might doesn't necessarily mean these objects are interchangeable (again, see Fluent Python, Ch. 11, by Luciano Ramalho)! An ABC gives you the ability to make a clear declaration of intent. Also, you can create a virtual subclass by registering the class so you don't have to subclass from it (in this sense, you are following the GoF principle of "favoring composition over inheritance" by not tying yourself directly to the ABC).
Raymond Hettinger gives an excellent talk on ABCs in the collections module in his PyCon 2019 Talk.
Also, Alex Martelli called ABCs goose typing. You can subclass many of the classes in collections.abc, implement only a few methods, and have classes behave like the builtin Python protocols implemented with dunder methods.

Luciano Ramalho gives an excellent talk on this and its relationship to the typing ecosystem in his PyCon 2021 Talk.
Incorrect Approaches
@overload
@overload is designed to be used to mimic functional polymorphism.
def func(a: int, b: str, c: bool) -> str:
print(f'{a}, {b}, {c}')
def func(a: int, b: bool) -> str:
print(f'{a}, {b}')
if __name__ == '__main__':
func(1, '2', True) # Error: `func()` takes 2 positional arguments but 3 were given
Python mimics functional polymorphism with optional positional/keyword arguments (coincidentally, C++ does not support keywrod arguments).
Overloads are to be used when
- (1) typing ported C/C++ polymorphic functions, or
- (2) type consistency must be maintained between depending on types used in a function call
Please see Adam Johnson's blog post "Python Type Hints - How to Use @overload.
References
(1) Ramalho, Luciano. Fluent Python (p. 320). O'Reilly Media. Kindle Edition.