Skip to Content
Course content

313: Overload Signatures with @overload

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen a dozen times in production codebases. You want a function to be flexible—maybe it can take a single ID and return a single object, or it can take a list of IDs and return a list of objects. It seems like a clean API, right? But when you try to type it, you hit a wall.

When Union types leave you guessing

Take a look at this common pattern. I'm trying to fetch user records from a database. If I pass an integer, I want a User object. If I pass a list of integers, I want a list of User objects.

from typing import Union

class User:
    def __init__(self, id: int):
        self.id = id
        self.name = "Developer"

def get_users(user_ids: Union[int, list[int]]) -> Union[User, list[User]]:
    if isinstance(user_ids, list):
        return [User(uid) for uid in user_ids]
    return User(user_ids)

# The logic works fine at runtime, but look at the type checker:
user = get_users(123)
print(user.name)  # Type checker error: "User | list[User]" has no attribute "name"

Here is the frustration: Python's type checker (like Mypy or Pyright) knows that user is either a User or a list[User], but it doesn't "remember" that because you passed an int, the result must be a single User. To the type checker, the relationship between the input type and the output type is lost. You end up having to write annoying isinstance checks every single time you call the function just to satisfy the IDE.

Linking inputs to outputs with @overload

This is exactly why @overload exists. It allows us to define multiple "signatures" for a function. Think of these as a map for the type checker: "If you see X as the input, expect Y as the output."

from typing import overload, Union

class User:
    def __init__(self, id: int):
        self.id = id
        self.name = "Developer"

@overload
def get_users(user_ids: int) -> User: ...

@overload
def get_users(user_ids: list[int]) -> list[User]: ...

def get_users(user_ids: Union[int, list[int]]) -> Union[User, list[User]]:
    if isinstance(user_ids, list):
        return [User(uid) for uid in user_ids]
    return User(user_ids)

# Now the magic happens:
user = get_users(123)        # Type checker knows this is 'User'
users = get_users([1, 2, 3]) # Type checker knows this is 'list[User]'

print(user.name) # No more errors!

Notice that I used ... (the ellipsis) in the overloaded signatures. This is because those functions don't actually "do" anything; they are strictly there for the type checker to read. I've essentially provided a cheat sheet to the IDE before the actual function logic begins.

The implementation catch

There is one thing that trips people up: the final function implementation. You cannot just write the overloads and walk away. You still need one final, actual function body that handles all the possible types defined in your overloads.

That final implementation (the one without the @overload decorator) is what actually runs at runtime. Interestingly, the type checker ignores the return type of the final implementation when you call the function from the outside—it only cares about the @overload signatures. However, you still need to make sure your logic inside that final function actually supports everything you promised in the signatures, or you'll be lying to your users (and yourself).

I generally recommend keeping the final implementation's type hints as broad Unions, as shown in the example above, to ensure it covers every overload case.




📋 Practical Task

Implementing a Flexible Data Parser

You are building a data processing utility. You need to write a function called parse_payload that behaves differently based on the input type:

  • If the input is a str, it should return a dict[str, str] (simulating parsing a JSON string).
  • If the input is a list[str], it should return a list[dict[str, str]] (simulating parsing multiple JSON strings).

Your task: Use @overload to ensure that when a user passes a string, the IDE recognizes the return value as a dictionary, and when they pass a list, the IDE recognizes it as a list of dictionaries. Implement the final function logic so it actually performs the "parsing" (you can just wrap the strings in a dictionary for this exercise).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.