Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
313: Overload Signatures with @overload
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 adict[str, str](simulating parsing a JSON string). - If the input is a
list[str], it should return alist[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).
There are no comments for now.