As @jonrsharpe noted in a comment, this can be done with collections.abc.Callable:

from collections.abc import Callable

def my_function(func: Callable):

Note: Callable on its own is equivalent to Callable[..., Any]. Such a Callable takes any number and type of arguments (...) and returns a value of any type (Any). If this is too unconstrained, one may also specify the types of the input argument list and return type.

For example, given:

def sum(a: int, b: int) -> int: return a+b

The corresponding annotation is:

Callable[[int, int], int]

That is, the parameters are sub-scripted in the outer subscription with the return type as the second element in the outer subscription. In general:

Callable[[ParamType1, ParamType2, ..., ParamTypeN], ReturnType]
Answer from Dimitris Fasarakis Hilliard on Stack Overflow
🌐
Python documentation
docs.python.org › 3 › library › typing.html
typing — Support for type hints
The argument list must be a list of types, a ParamSpec, Concatenate, or an ellipsis (...). The return type must be a single type. If a literal ellipsis ... is given as the argument list, it indicates that a callable with any arbitrary parameter list would be acceptable: def concat(x: str, y: str) -> str: return x + y x: Callable[..., str] x = str # OK x = concat # Also OK · Callable cannot express complex signatures such as functions that take a variadic number of arguments, overloaded functions, or functions that have keyword-only parameters.
🌐
Python
peps.python.org › pep-0484
PEP 484 – Type Hints | peps.python.org
Type checkers are expected to attempt to infer as much information as necessary. The minimum requirement is to handle the builtin decorators @property, @staticmethod and @classmethod. The syntax leverages PEP 3107-style annotations with a number of extensions described in sections below. In its basic form, type hinting is used by filling function annotation slots with classes: def greeting(name: str) -> str: return 'Hello ' + name · This states that the expected type of the name argument is str.
🌐
The Python Coding Book
thepythoncodingbook.com › home › blog › using type hints when defining a python function [intermediate python functions series #6]
Using type hints when defining a Python function
March 19, 2023 - Type hints indicate that the data the function returns is a list of strings. Therefore the IDE “knows” that item should be a str in the final for loop since result is a list of strings.
🌐
Reddit
reddit.com › r/learnpython › what is the point of type hinting when python doesn't even respect it!?
r/learnpython on Reddit: What is the point of type hinting when Python doesn't even respect it!?
August 15, 2024 -

So infuriating because I feel like my function is lying to me when it ends up letting other objects through the parameter.

Now every function I created needs an instance check which is unintuitive, verbose, and easily forgotten.

def wtf(string: str, integer:int):
   return 'TYPE HINT DOESNT DO ANYTHING' 

print( wtf( 123, 'wtf')  )
>> 'TYPE HINT DOESNT DO ANYTHING'

EDIT:

Turns out it is a noob mistake. Type hint only functions as a signal for IDE, but developers get to do whatever they want to your parameter.

If you really must enforce it, you got to have the line

if not isinstance(string, str): 
  raise TypeError()

Still, I don't like that this behavior isn't taught until it happens. So much time wasted on debugging production code when you take for granted your function is accepting only restricted data when it doesn't.

🌐
Open Water Foundation
learn.openwaterfoundation.org › owf-learn-python › lessons › type-hints › type-hints
Type Hints - OWF Learn Python
The build-in file type in Python requires the following type hint: from typing import TextIO def some_function(text_file_pointer: TypeIO) -> None: """ Example of passing a `file` type. """ pass · Python allows passing a function name as an argument to a function.
🌐
Mypy
mypy.readthedocs.io › en › stable › cheat_sheet_py3.html
Type hints cheat sheet - mypy 1.19.1 documentation
# For most types, just use the name of the type in the annotation # Note that mypy can usually infer the type of a variable from its value, # so technically these annotations are redundant x: int = 1 x: float = 1.0 x: bool = True x: str = "test" x: bytes = b"test" # For collections on Python 3.9+, the type of the collection item is in brackets x: list[int] = [1] x: set[int] = {6, 7} # For mappings, we need the types of both keys and values x: dict[str, float] = {"field": 2.0} # Python 3.9+ # For tuples of fixed size, we specify the types of all the elements x: tuple[int, str, float] = (3, "yes
🌐
Medium
medium.com › @AlexanderObregon › how-pythons-type-hinting-and-annotations-work-319d952247a6
How Python’s Type Hinting and Annotations Work | Medium
July 14, 2024 - The type hints make it clear what type of argument the function expects and what type it will return. Type hints are not limited to basic data types like int, str, and float. They can also be used with collections to specify the types of elements ...
Find elsewhere
🌐
Towards Data Science
towardsdatascience.com › home › latest › type hints in python
Type Hints in Python | Towards Data Science
January 16, 2025 - What I did here was declare the data type, which is just a list of data frames, and use it as a type hint. Also, something we haven’t seen before today, this function doesn’t return anything. That’s why the output data type is None. It’s often that we create functions in which some arguments aren’t required – they’re optional.
🌐
Real Python
realpython.com › python-type-hints-multiple-types
How to Use Type Hints for Multiple Return Types in Python – Real Python
March 8, 2024 - If the return type can be deduced from the argument types, then you can alternatively use @overload to specify different type signatures. Now that you know how to define a function that returns a single value of potentially different types, you can turn your attention toward using type hints to declare that a function can return more than one piece of data. ... Sometimes, a function returns more than one value, and you can communicate this in Python using type hints.
🌐
Dagster
dagster.io › blog › python-type-hinting
Using Type Hinting in Python Projects
Here, age is hinted as an integer, name as a string, and is_active as a boolean. You can provide type hints for function parameters and return values. This helps other developers understand what types of arguments are expected by the function and what type the function returns. def greet(name: str) -> str: \ return f"Hello, {name}" In this example, the function greet expects name to be a string and will return a string. Python has several built-in types.
🌐
Real Python
realpython.com › lessons › type-hinting
Type Hinting (Video) – Real Python
Type hinting is a formal solution to statically indicate the type of a value within your Python code. It was specified in PEP 484 and introduced in Python 3.5. Here’s an example of adding type information to a function.
Published   October 29, 2019
🌐
Reddit
reddit.com › r/python › why type hinting sucks!
r/Python on Reddit: Why Type Hinting Sucks!
February 13, 2023 -

Type hints are great! But I was playing Devil's advocate on a thread recently where I claimed actually type hinting can be legitimately annoying, especially to old school Python programmers.

But I think a lot of people were skeptical, so let's go through a made up scenario trying to type hint a simple Python package. Go to the end for a TL;DR.

The scenario

This is completely made up, all the events are fictitious unless explicitly stated otherwise (also editing this I realized attempts 4-6 have even more mistakes in them than intended but I'm not rewriting this again):

You maintain a popular third party library slowadd, your library has many supporting functions, decorators, classes, and metaclasses, but your main function is:

def slow_add(a, b):
    time.sleep(0.1)
    return a + b

You've always used traditional Python duck typing, if a and b don't add then the function throws an exception. But you just dropped support for Python 2 and your users are demanding type hinting, so it's your next major milestone.

First attempt at type hinting

You update your function:

def slow_add(a: int, b: int) -> int:
    time.sleep(0.1)
    return a + b

All your tests pass, mypy passes against your personal code base, so you ship with the release note "Type Hinting Support added!"

Second attempt at type hinting

Users immediately flood your GitHub issues with complaints! MyPy is now failing for them because they pass floats to slow_add, build processes are broken, they can't downgrade because of internal Enterprise policies of always having to increase type hint coverage, their weekend is ruined from this issue.

You do some investigating and find that MyPy supports Duck type compatibility for ints -> floats -> complex. That's cool! New release:

def slow_add(a: complex, b: complex) -> complex:
    time.sleep(0.1)
    return a + b

Funny that this is a MyPy note and not a PEP standard...

Third attempt at type hinting

Your users thank you for your quick release, but a couple of days later one user asks why you no longer support Decimal. You replace complex with Decimal but now your other MyPy tests are failing.

You remember Python 3 added Numeric abstract base classes, what a perfect use case, just type hint everything as numbers.Number.

Hmmm, MyPy doesn't consider any of integers, or floats, or Decimals to be numbers :(.

After reading through typing you guess you'll just Union in the Decimals:

def slow_add(
    a: Union[complex, Decimal], b: Union[complex, Decimal]
) -> Union[complex, Decimal]:
    time.sleep(0.1)
    return a + b

Oh no! MyPy is complaining that you can't add your other number types to Decimals, well that wasn't your intention anyway...

More reading later and you try overload:

@overload
def slow_add(a: Decimal, b: Decimal) -> Decimal:
    ...

@overload
def slow_add(a: complex, b: complex) -> complex:
    ...

def slow_add(a, b):
    time.sleep(0.1)
    return a + b

But MyPy on strict is complaining that slow_add is missing a type annotation, after reading this issue you realize that @overload is only useful for users of your function but the body of your function will not be tested using @overload. Fortunately in the discussion on that issue there is an alternative example of how to implement:

T = TypeVar("T", Decimal, complex)

def slow_add(a: T, b: T) -> T:
    time.sleep(0.1)
    return a + b

Fourth attempt at type hinting

You make a new release, and a few days later more users start complaining. A very passionate user explains the super critical use case of adding tuples, e.g. slow_add((1, ), (2, ))

You don't want to start adding each type one by one, there must be a better way! You learn about Protocols, and Type Variables, and positional only parameters, phew, this is a lot but this should be perfect now:

T = TypeVar("T")

class Addable(Protocol):
    def __add__(self: T, other: T, /) -> T:
        ...

def slow_add(a: Addable, b: Addable) -> Addable:
    time.sleep(0.1)
    return a + b

A mild diversion

You make a new release noting "now supports any addable type".

Immediately the tuple user complains again and says type hints don't work for longer Tuples: slow_add((1, 2), (3, 4)). That's weird because you tested multiple lengths of Tuples and MyPy was happy.

After debugging the users environment, via a series of "back and forth"s over GitHub issues, you discover that pyright is throwing this as an error but MyPy is not (even in strict mode). You assume MyPy is correct and move on in bliss ignoring there is actually a fundamental mistake in your approach so far.

(Author Side Note - It's not clear if MyPy is wrong but it defiantly makes sense for Pyright to throw an error here, I've filed issues against both projects and a pyright maintainer has explained the gory details if you're interested. Unfortunately this was not really addressed in this story until the "Seventh attempt")

Fifth attempt at type hinting

A week later a user files an issue, the most recent release said that "now supports any addable type" but they have a bunch of classes that can only be implemented using __radd__ and the new release throws typing errors.

You try a few approaches and find this seems to best solve it:

T = TypeVar("T")

class Addable(Protocol):
    def __add__(self: T, other: T, /) -> T:
        ...

class RAddable(Protocol):
    def __radd__(self: T, other: Any, /) -> T:
        ...

@overload
def slow_add(a: Addable, b: Addable) -> Addable:
    ...

@overload
def slow_add(a: Any, b: RAddable) -> RAddable:
    ...

def slow_add(a: Any, b: Any) -> Any:
    time.sleep(0.1)
    return a + b

Annoyingly there is now no consistent way for MyPy to do anything with the body of the function. Also you weren't able to fully express that when b is "RAddable" that "a" should not be the same type because Python type annotations don't yet support being able to exclude types.

Sixth attempt at type hinting

A couple of days later a new user complains they are getting type hint errors when trying to raise the output to a power, e.g. pow(slow_add(1, 1), slow_add(1, 1)). Actually this one isn't too bad, you quick realize the problem is your annotating Protocols, but really you need to be annotating Type Variables, easy fix:

T = TypeVar("T")

class Addable(Protocol):
    def __add__(self: T, other: T, /) -> T:
        ...

A = TypeVar("A", bound=Addable)

class RAddable(Protocol):
    def __radd__(self: T, other: Any, /) -> T:
        ...

R = TypeVar("R", bound=RAddable)

@overload
def slow_add(a: A, b: A) -> A:
    ...

@overload
def slow_add(a: Any, b: R) -> R:
    ...

def slow_add(a: Any, b: Any) -> Any:
    time.sleep(0.1)
    return a + b

Seventh attempt at type hinting

Tuple user returns! He says MyPy in strict mode is now complaining with the expression slow_add((1,), (2,)) == (1, 2) giving the error:

Non-overlapping equality check (left operand type: "Tuple[int]", right operand type: "Tuple[int, int]")

You realize you can't actually guarantee anything about the return type from some arbitrary __add__ or __radd__, so you starting throwing Any Liberally around:

class Addable(Protocol):
    def __add__(self: "Addable", other: Any, /) -> Any:
        ...

class RAddable(Protocol):
    def __radd__(self: "RAddable", other: Any, /) -> Any:
        ...

@overload
def slow_add(a: Addable, b: Any) -> Any:
    ...

@overload
def slow_add(a: Any, b: RAddable) -> Any:
    ...

def slow_add(a: Any, b: Any) -> Any:
    time.sleep(0.1)
    return a + b

Eighth attempt at type hinting

Users go crazy! The nice autosuggestions their IDE provided them in the previous release have all gone! Well you can't type hint the world, but I guess you could include type hints for the built-in types and maybe some Standard Library types like Decimal:

You think you can rely on some of that MyPy duck typing but you test:

@overload
def slow_add(a: complex, b: complex) -> complex:
    ...

And realize that MyPy throws an error on something like slow_add(1, 1.0).as_integer_ratio(). So much for that nice duck typing article on MyPy you read earlier.

So you end up implementing:

class Addable(Protocol):
    def __add__(self: "Addable", other: Any, /) -> Any:
        ...

class RAddable(Protocol):
    def __radd__(self: "RAddable", other: Any, /) -> Any:
        ...

@overload
def slow_add(a: int, b: int) -> int:
    ...

@overload
def slow_add(a: float, b: float) -> float:
    ...

@overload
def slow_add(a: complex, b: complex) -> complex:
    ...

@overload
def slow_add(a: str, b: str) -> str:
    ...

@overload
def slow_add(a: tuple[Any, ...], b: tuple[Any, ...]) -> tuple[Any, ...]:
    ...

@overload
def slow_add(a: list[Any], b: list[Any]) -> list[Any]:
    ...

@overload
def slow_add(a: Decimal, b: Decimal) -> Decimal:
    ...

@overload
def slow_add(a: Fraction, b: Fraction) -> Fraction:
    ...

@overload
def slow_add(a: Addable, b: Any) -> Any:
    ...

@overload
def slow_add(a: Any, b: RAddable) -> Any:
    ...

def slow_add(a: Any, b: Any) -> Any:
    time.sleep(0.1)
    return a + b

As discussed earlier MyPy doesn't use the signature of any of the overloads and compares them to the body of the function, so all these type hints have to manually validated as accurate by you.

Ninth attempt at type hinting

A few months later a user says they are using an embedded version of Python and it hasn't implemented the Decimal module, they don't understand why your package is even importing it given it doesn't use it. So finally your code looks like:

from __future__ import annotations

import time
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, overload

if TYPE_CHECKING:
    from decimal import Decimal
    from fractions import Fraction


class Addable(Protocol):
    def __add__(self: "Addable", other: Any, /) -> Any:
        ...

class RAddable(Protocol):
    def __radd__(self: "RAddable", other: Any, /) -> Any:
        ...

@overload
def slow_add(a: int, b: int) -> int:
    ...

@overload
def slow_add(a: float, b: float) -> float:
    ...

@overload
def slow_add(a: complex, b: complex) -> complex:
    ...

@overload
def slow_add(a: str, b: str) -> str:
    ...

@overload
def slow_add(a: tuple[Any, ...], b: tuple[Any, ...]) -> tuple[Any, ...]:
    ...

@overload
def slow_add(a: list[Any], b: list[Any]) -> list[Any]:
    ...

@overload
def slow_add(a: Decimal, b: Decimal) -> Decimal:
    ...

@overload
def slow_add(a: Fraction, b: Fraction) -> Fraction:
    ...

@overload
def slow_add(a: Addable, b: Any) -> Any:
    ...

@overload
def slow_add(a: Any, b: RAddable) -> Any:
    ...

def slow_add(a: Any, b: Any) -> Any:
    time.sleep(0.1)
    return a + b

TL;DR

Turning even the simplest function that relied on Duck Typing into a Type Hinted function that is useful can be painfully difficult.

Please always put on your empathetic hat first when asking someone to update their code to how you think it should work.

In writing up this post I learnt a lot about type hinting, please try and find edge cases where my type hints are wrong or could be improved, it's a good exercise.

Edit: Had to fix a broken link.

Edit 2: It was late last night and I gave up on fixing everything, some smart people nicely spotted the errors!

I have a "tenth attempt" to address these error. But pyright complains about it because my overloads overlap, however I don't think there's a way to express what I want in Python annotations without overlap. Also Mypy complains about some of the user code I posted earlier giving the error comparison-overlap, interestingly though pyright seems to be able to detect here that the types don't overlap in the user code.

I'm going to file issues on pyright and mypy, but fundamentally they might be design choices rather than strictly bugs and therefore a limit on the current state of Python Type Hinting:

T = TypeVar("T")

class SameAddable(Protocol):
    def __add__(self: T, other: T, /) -> T:
        ...

class Addable(Protocol):
    def __add__(self: "Addable", other: Any, /) -> Any:
        ...

class SameRAddable(Protocol):
    def __radd__(self: T, other: Any, /) -> T:
        ...

class RAddable(Protocol):
    def __radd__(self: "RAddable", other: Any, /) -> Any:
        ...

SA = TypeVar("SA", bound=SameAddable)
RA = TypeVar("RA", bound=SameRAddable)


@overload
def slow_add(a: SA, b: SA) -> SA:
    ...

@overload
def slow_add(a: Addable, b: Any) -> Any:
    ...

@overload
def slow_add(a: Any, b: RA) -> RA:
    ...

@overload
def slow_add(a: Any, b: RAddable) -> Any:
    ...

def slow_add(a: Any, b: Any) -> Any:
    time.sleep(0.1)
    return a + b
🌐
Adam Johnson
adamj.eu › tech › 2021 › 09 › 06 › python-type-hints-how-to-vary-return-type-based-on-an-argument
Python type hints: vary return type based on an argument - Adam Johnson
Here’s a recipe that combines typing.Literal with @overload to define a function that switches its return type based on the value of an argument.
🌐
GeeksforGeeks
geeksforgeeks.org › python › type-hints-in-python
Type Hints in Python - GeeksforGeeks
May 3, 2025 - Type hints define the expected type for variables and function arguments. Example: ... The return type of a function can be specified using ->. Example: ... Explanation: The add_numbers function expects two integers (x: int, y: int) and returns ...
🌐
EITCA
eitca.org › home › what is type hinting and how can it be used to specify the expected types of function parameters?
What is type hinting and how can it be used to specify the expected types of function parameters? - EITCA Academy
August 3, 2023 - python from typing import Optional ... If no value is provided for `name`, it defaults to `None`. Type hinting can also be used with variable-length argument lists using the `*args` and `**kwargs` syntax....
🌐
Python Tutorial
pythontutorial.net › home › python basics › python type hints
Python Type Hints
April 2, 2025 - If you change back the argument to a string and run the mypy again, it’ll show a success message: Success: no issues found in 1 source fileCode language: Python (python) When defining a variable, you can add a type hint like this: ... The type of name variable is str. If you assign a value ...
🌐
Reddit
reddit.com › r/python › what is too much type hinting for you?
r/Python on Reddit: What is too much type hinting for you?
July 27, 2024 -

For me it's :

from typing import Self

class Foo:
    def __init__(self: Self) -> None:
        ...

The second example is acceptable in my opinion, as the parameter are one type and the type hint for the actual attributes is for their entire lifetimes within the instance :

class Foo:
    def __init__(self, par1: int, par2: tuple[float, float]):
        self.par1: int = par1
        self.par2: tuple[float, float] | None = par2

Edit: changed the method in the first example from bar to __init__

🌐
Towards Data Science
towardsdatascience.com › home › latest › type hints in python – everything you need to know in 5 minutes
Type Hints in Python - Everything You Need To Know In 5 Minutes | Towards Data Science
January 30, 2025 - The function accepts the integer and squares it, at least according to type hints. Upon execution, the function is evaluated both with an integer and a float. Here’s what happens when using the default Python interpreter: Image 1 – Using the default Python interpreter (image by author) ... As you can see, mypy works as advertised, so feel free to use it when required.