When I first started combining default parameters with type hinting in Python, I ran into confusion. At first glance, it seemed simple but when mutable defaults like dictionaries came into play, I quickly realized things weren’t as straightforward as I thought.
Let me walk you through my journey step by step, the mistakes I made, and how I finally fixed it.
The First Attempt (Problematic Code)
I began with something simple:
def foo(name, opts={}):
pass
At first glance, this looks fine. If no options are passed, opts
defaults to an empty dictionary.
But here’s the catch:
- This introduces a classic Python bug.
- Since
{}
is mutable, the same dictionary persists across function calls. - That means if the dictionary is modified in one call, the changes leak into future calls.
This isn’t what I wanted at all.
Adding Type Hints the Wrong Way
My next step was to add type hints. I tried this:
def foo(name: str, opts={}: dict) -> str:
pass
And boom syntax error.
The reason? Python expects the type before the default value, not after. So my placement was wrong.
The “Works but Misleading” Way
Next, I tried:
def foo(name: str, opts: dict = {}) -> str:
pass
This worked syntactically, but it still suffered from the mutable default argument problem. Multiple calls would share the same dictionary instance. Not safe at all.
So while it looked okay, it wasn’t the right solution.
The Correct Way
Finally, I discovered the right approach: use None
as the default, and create a new dictionary inside the function when needed.
from typing import Optional
def foo(name: str, opts: Optional[dict] = None) -> str:
if opts is None:
opts = {}
return f"Hello {name}, with options: {opts}"
Why this works:
- Type clarity:
opts
is either adict
orNone
. - Safe defaults: A new dictionary is created each time.
- Readable code: Anyone reading this immediately understands the intention.
Adding More Practice Functionality
Once I had the foundation fixed, I decided to make the function more practical type hinting. What if I wanted to customize greetings using the dictionary?
Here’s my expanded version:
from typing import Optional, Dict
def foo(name: str, opts: Optional[Dict[str, str]] = None) -> str:
if opts is None:
opts = {}
greeting = opts.get("greeting", "Hello")
punctuation = opts.get("punctuation", "!")
style = opts.get("style", "plain")
message = f"{greeting}, {name}{punctuation}"
if style == "upper":
return message.upper()
elif style == "lower":
return message.lower()
return message
Example Usage
print(foo("Alice"))
# Output: Hello, Alice!
print(foo("Bob", {"greeting": "Hi"}))
# Output: Hi, Bob!
print(foo("Charlie", {"style": "upper", "punctuation": "!!"}))
# Output: HELLO, CHARLIE!!
With this approach, I can safely use type hints, avoid bugs, and add flexibility to my function.
Final Thought
In the end, fixing default parameters with type hinting in Python comes down to avoiding mutable defaults and using None
wisely. By combining Optional
type hints with safe initialization inside the function, I get cleaner, more reliable, and bug-free code. It’s a small adjustment that makes a big difference in writing professional Python functions.