Python Overload Practice Problems & Exercises
Practice: Overload and Type Narrowing
← Back to lessonEasy
Use @overload to declare that double(x: int) -> int, double(x: str) -> str, and double(x: float) -> float. Implement the actual function.
from typing import Union, overload
@overload
def double(x: int) -> int: ...
@overload
def double(x: str) -> str: ...
@overload
def double(x: float) -> float: ...
def double(x: Union[int, str, float]) -> Union[int, str, float]:
if isinstance(x, int) and not isinstance(x, bool):
return x * 2
elif isinstance(x, float):
return x * 2.0
elif isinstance(x, str):
return x * 2
raise TypeError(f"Unsupported type: {type(x)}")
print(f"double(5) = {double(5)}")
print(f"double('hi') = {double('hi')}")
print(f"double(3.14) = {double(3.14)}")Expected Output
double(5) = 10
double('hi') = hihi
double(3.14) = 6.28Hints
Hint 1: @overload stubs declare the different signatures. The actual implementation uses Union and handles all cases.
Hint 2: The implementation body is NOT decorated with @overload — only the stubs are.
Write a process function that uses isinstance to narrow a Union[int, str, list] and applies type-specific operations to each branch.
from typing import Union, List
def process(value: Union[int, str, List[int], object]) -> str:
if isinstance(value, int):
return f"Processing int: {value} squared = {value ** 2}"
elif isinstance(value, str):
return f"Processing str: {value!r} upper = {value.upper()}"
elif isinstance(value, list):
return f"Processing list: {value} sum = {sum(value)}"
else:
return f"Unsupported: {type(value)}"
print(process(42))
print(process("hello"))
print(process([1, 2, 3]))
print(process({"key": "val"}))Expected Output
Processing int: 42 squared = 1764
Processing str: 'hello' upper = HELLO
Processing list: [1,2,3] sum = 6
Unsupported: <class 'dict'>Hints
Hint 1: After isinstance(x, int), the static checker narrows x to int in that branch. You can safely call int-specific operations.
Hint 2: The else branch still has the original Union type, so narrowing is sequential.
Use Literal types to write a get_config(mode) function where mode can only be "development", "production", or "staging". Return different config dicts for each.
from typing import Literal, Dict, Any
def get_config(
mode: Literal["development", "production", "staging"]
) -> Dict[str, Any]:
if mode == "development":
return {"debug": True, "db": "sqlite:///dev.db", "mode": mode}
elif mode == "production":
return {"debug": False, "db": "postgresql://prod/db", "mode": mode}
else: # staging — narrowed to exactly "staging"
return {"debug": True, "db": "postgresql://staging/db", "mode": mode}
for env in ("development", "production", "staging"):
cfg = get_config(env) # type: ignore[arg-type]
print(f"{env} config: debug={cfg['debug']}, db={cfg['db']}")Expected Output
development config: debug=True, db=sqlite:///dev.db
production config: debug=False, db=postgresql://prod/db
staging config: debug=True, db=postgresql://staging/dbHints
Hint 1: Declare mode: Literal["development", "production", "staging"] as the parameter type.
Hint 2: Each branch after if mode == "development" narrows mode to exactly that Literal.
Medium
Write an is_int_list TypeGuard that verifies every element is an int. Use it to narrow a List[Any] before calling sum().
from typing import List, Any
try:
from typing import TypeGuard
except ImportError:
from typing_extensions import TypeGuard
def is_int_list(lst: List[Any]) -> "TypeGuard[List[int]]":
return all(isinstance(item, int) for item in lst)
def process_lists(lists: List[List[Any]]) -> None:
for lst in lists:
if is_int_list(lst):
print(f"{lst} is int list: True")
print(f"Sum of int list: {sum(lst)}")
else:
print(f"{lst} is int list: False")
print("Mixed list skipped")
data = [
[1, 2, 3],
[1, "a", 3],
]
process_lists(data)Expected Output
[1, 2, 3] is int list: True
[1, 'a', 3] is int list: False
Sum of int list: 6
Mixed list skippedHints
Hint 1: Annotate the guard function as def is_int_list(lst: List[Any]) -> TypeGuard[List[int]].
Hint 2: After the if is_int_list(x) check, the static checker narrows x to List[int] in the true branch.
Implement an overloaded SafeDict.get(key, default=...) where the return type is V (not Optional[V]) when a non-None default is provided.
from typing import TypeVar, Dict, Optional, overload, Union
K = TypeVar("K")
V = TypeVar("V")
class SafeDict(Dict[str, int]):
@overload
def get(self, key: str) -> Optional[int]: ...
@overload
def get(self, key: str, default: None) -> Optional[int]: ...
@overload
def get(self, key: str, default: int) -> int: ...
def get(self, key: str, default: Optional[int] = None) -> Optional[int]:
return super().get(key, default)
d = SafeDict({"a": 1, "b": 2, "c": 3})
print(f"get('a') = {d.get('a')}")
print(f"get('z') = {d.get('z')}")
print(f"get('z', 0) = {d.get('z', 0)}")
print(f"get('a', 99) = {d.get('a', 99)}")Expected Output
get('a') = 1
get('z') = None
get('z', 0) = 0
get('a', 99) = 1Hints
Hint 1: Three overloads: get(key) -> Optional[V], get(key, None) -> Optional[V], get(key, default: V) -> V.
Hint 2: The default=None overload returns Optional[V]; the default: V overload guarantees a V is returned.
Use Literal discriminants in TypedDict shapes to enable exhaustive narrowing. Write an area(shape) function that handles all three tagged union variants.
from typing import Union, Literal
import math
try:
from typing import TypedDict
except ImportError:
from typing_extensions import TypedDict
class CircleShape(TypedDict):
kind: Literal["circle"]
radius: float
class RectangleShape(TypedDict):
kind: Literal["rectangle"]
width: float
height: float
class TriangleShape(TypedDict):
kind: Literal["triangle"]
base: float
height: float
Shape = Union[CircleShape, RectangleShape, TriangleShape]
def area(shape: Shape) -> float:
if shape["kind"] == "circle":
return math.pi * shape["radius"] ** 2
elif shape["kind"] == "rectangle":
return shape["width"] * shape["height"]
else: # triangle — narrowed by exhaustion
return 0.5 * shape["base"] * shape["height"]
shapes: list = [
{"kind": "circle", "radius": 5.0},
{"kind": "rectangle", "width": 4.0, "height": 6.0},
{"kind": "triangle", "base": 3.0, "height": 4.0},
]
for s in shapes:
print(f"{s['kind'].capitalize()} area: {area(s)}") # type: ignore[arg-type]Expected Output
Circle area: 78.53981633974483
Rectangle area: 24
Triangle area: 6.0Hints
Hint 1: Each shape has a kind: Literal["circle"] / kind: Literal["rectangle"] / kind: Literal["triangle"]. This discriminant enables exhaustive narrowing.
Hint 2: After if shape['kind'] == 'circle', the checker knows it's the Circle TypedDict.
Use @overload to write a parse function that accepts either a single string or a list of strings and converts them to int or float depending on the second argument.
from typing import List, Union, Type, overload
@overload
def parse(value: str, type_: Type[int]) -> int: ...
@overload
def parse(value: List[str], type_: Type[int]) -> List[int]: ...
@overload
def parse(value: str, type_: Type[float]) -> float: ...
@overload
def parse(value: List[str], type_: Type[float]) -> List[float]: ...
def parse(
value: Union[str, List[str]],
type_: Union[Type[int], Type[float]] = int,
) -> Union[int, float, List[int], List[float]]:
if isinstance(value, list):
return [type_(v) for v in value] # type: ignore[arg-type]
return type_(value) # type: ignore[arg-type]
print(f"parse('42') = {parse('42', int)}")
print(f"parse(['1','2','3']) = {parse(['1','2','3'], int)}")
print(f"parse('3.14', float) = {parse('3.14', float)}")
print(f"parse(['1.0','2.0'], float) = {parse(['1.0','2.0'], float)}")Expected Output
parse('42') = 42
parse(['1','2','3']) = [1, 2, 3]
parse('3.14', float) = 3.14
parse(['1.0','2.0'], float) = [1.0, 2.0]Hints
Hint 1: Four overloads: parse(str, int) -> int, parse(List[str], int) -> List[int], parse(str, float) -> float, parse(List[str], float) -> List[float].
Hint 2: The implementation uses isinstance to distinguish str from list, then delegates to the type_ callable.
Hard
Write an exhaustive currency formatter using Literal union. Add an assert_never helper in the default branch to catch any future unhandled currency codes at compile time.
from typing import Literal, Union, NoReturn
Currency = Literal["USD", "EUR", "GBP", "JPY"]
def assert_never(x: NoReturn) -> NoReturn:
raise AssertionError(f"Unhandled currency: {x!r}")
def format_amount(amount: float, currency: Currency) -> str:
if currency == "USD":
return f"${amount:.2f}"
elif currency == "EUR":
return f"{amount:.2f} EUR"
elif currency == "GBP":
return f"{amount:.2f} GBP"
elif currency == "JPY":
return f"{int(amount)} JPY"
else:
assert_never(currency)
currencies: list = ["USD", "EUR", "GBP", "JPY"]
for c in currencies:
print(f"{c}: {format_amount(100.0, c)}") # type: ignore[arg-type]Expected Output
USD: $100.00
EUR: 100.00 EUR
GBP: 100.00 GBP
JPY: 100 JPYHints
Hint 1: Use a Literal union for currency codes. Write an assert_never(x: NoReturn) helper that raises AssertionError — it is called in the else branch to catch unhandled cases.
Hint 2: A static checker will flag the else branch as unreachable if all Literal values are covered.
Write a TypeGuard-based validator for a nested EventPayload TypedDict. Use it to safely access deeply nested fields after narrowing.
from typing import Any, Dict
try:
from typing import TypedDict, TypeGuard
except ImportError:
from typing_extensions import TypedDict, TypeGuard
class UserInfo(TypedDict):
id: int
name: str
class EventPayload(TypedDict):
event: str
user: UserInfo
timestamp: float
def is_event_payload(data: Dict[str, Any]) -> "TypeGuard[EventPayload]":
if not isinstance(data, dict):
return False
if "event" not in data or not isinstance(data["event"], str):
return False
if "timestamp" not in data or not isinstance(data["timestamp"], (int, float)):
return False
user = data.get("user")
if not isinstance(user, dict):
return False
if "id" not in user or not isinstance(user["id"], int):
return False
if "name" not in user or not isinstance(user["name"], str):
return False
return True
def handle_event(data: Dict[str, Any]) -> None:
if is_event_payload(data):
print(f"Valid payload: True")
print(f"user_id = {data['user']['id']}")
print(f"event = {data['event']}")
else:
print(f"Invalid payload: False")
print("Processing skipped")
valid = {
"event": "login",
"user": {"id": 42, "name": "Alice"},
"timestamp": 1711000000.0,
}
invalid = {
"event": "login",
"user": "not-a-dict", # wrong type
"timestamp": 1711000000.0,
}
handle_event(valid)
handle_event(invalid)Expected Output
Valid payload: True
user_id = 42
event = login
Invalid payload: False
Processing skippedHints
Hint 1: The TypeGuard function checks for the presence of all required keys and their types recursively.
Hint 2: Once the guard passes, the checker narrows the dict to the TypedDict shape inside the if block.
Use @overload to declare a fetch function that returns str when async_mode=False and a Coroutine when async_mode=True. Show both call paths work.
import asyncio
from typing import Union, overload
from typing import Coroutine, Any
@overload
def fetch(url: str, async_mode: "Literal[False]" = ...) -> str: ...
@overload
def fetch(url: str, async_mode: "Literal[True]") -> Coroutine[Any, Any, str]: ...
from typing import Literal
@overload
def fetch(url: str, async_mode: Literal[False] = ...) -> str: ...
@overload
def fetch(url: str, async_mode: Literal[True]) -> Coroutine[Any, Any, str]: ...
def fetch(
url: str,
async_mode: bool = False,
) -> Union[str, Coroutine[Any, Any, str]]:
async def _async_fetch() -> str:
await asyncio.sleep(0) # simulate async I/O
return f"data for {url}"
if async_mode:
return _async_fetch()
else:
return f"data for {url}"
# Sync path
sync_result = fetch("/api/users", async_mode=False)
print(f"sync fetch: {sync_result}")
# Async path
async def main() -> None:
async_result = await fetch("/api/orders", async_mode=True)
print(f"async fetch: {async_result}")
asyncio.run(main())
print("Both modes work: True")Expected Output
sync fetch: data for /api/users
async fetch: data for /api/orders
Both modes work: TrueHints
Hint 1: Two overloads: fetch(url, async_mode=False) -> str and fetch(url, async_mode=True) -> Coroutine[Any, Any, str].
Hint 2: The implementation checks async_mode and either returns directly or returns a coroutine object.
Chain three narrowing techniques — TypeGuard, Literal status narrowing, and @overload — in a single order-processing pipeline.
from typing import Dict, Any, List, Literal, Union, overload
try:
from typing import TypedDict, TypeGuard
except ImportError:
from typing_extensions import TypedDict, TypeGuard
OrderStatus = Literal["pending", "confirmed", "shipped", "cancelled"]
class OrderRequest(TypedDict):
order_id: int
customer: str
items: List[str]
status: str
def is_order_request(data: Dict[str, Any]) -> "TypeGuard[OrderRequest]":
required = {"order_id": int, "customer": str, "items": list, "status": str}
for key, expected in required.items():
if key not in data or not isinstance(data[key], expected):
return False
return True
@overload
def format_order(order: OrderRequest, status: Literal["pending"]) -> str: ...
@overload
def format_order(order: OrderRequest, status: Literal["confirmed"]) -> str: ...
@overload
def format_order(order: OrderRequest, status: str) -> str: ...
def format_order(order: OrderRequest, status: str) -> str:
return (
f"Order {order['order_id']} for {order['customer']} is {status} "
f"with {len(order['items'])} items"
)
def process_order(raw: Dict[str, Any]) -> str:
# Step 1: TypeGuard narrowing
if not is_order_request(raw):
return "Invalid order"
print(f"Step 1: raw dict validated: True")
# Step 2: now narrowed to OrderRequest
order: OrderRequest = raw
print(f"Step 2: typed as OrderRequest")
# Step 3: Literal narrowing on status
status = order["status"]
if status == "pending":
print(f"Step 3: status narrowed to 'pending'")
return format_order(order, "pending")
elif status == "confirmed":
return format_order(order, "confirmed")
else:
return format_order(order, status)
raw_order = {
"order_id": 123,
"customer": "Alice",
"items": ["widget", "gadget"],
"status": "pending",
}
result = process_order(raw_order)
print(f"Result: {result}")Expected Output
Step 1: raw dict validated: True
Step 2: typed as OrderRequest
Step 3: status narrowed to 'pending'
Result: Order 123 for Alice is pending with 2 itemsHints
Hint 1: Chain three narrowing steps: is_order_request TypeGuard, then Literal narrowing on status, then overloaded format_order.
Hint 2: Each step demonstrates a different narrowing technique — TypeGuard, Literal comparison, and overload.
