Python
type hinting
function parameters
default arguments
programming tips

How do I add default parameters to functions when using type hinting?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Python, default values and type hints use a simple syntax, but the details matter once None, mutable defaults, or static type checkers enter the picture. The goal is to make the function signature truthful: the annotation should describe what callers may pass, and the default should match that contract.

The Basic Syntax

The standard form is:

python
1def greet(name: str = "World", excited: bool = False) -> str:
2    message = f"Hello, {name}"
3    if excited:
4        message += "!"
5    return message
6
7print(greet())
8print(greet("Ada", excited=True))

The pattern is:

  • parameter name
  • colon and type annotation
  • equals sign and default value

That is all you need for many functions.

Match the Type to the Default

If the default is None, the type should say that None is allowed. In modern Python, that usually means str | None, list[int] | None, and similar forms.

python
1def find_user(user_id: int, nickname: str | None = None) -> dict[str, str | int]:
2    result: dict[str, str | int] = {"id": user_id}
3    if nickname is not None:
4        result["nickname"] = nickname
5    return result

In older style typing, the same idea is written with Optional:

python
1from typing import Optional
2
3def find_user_old(user_id: int, nickname: Optional[str] = None) -> dict[str, str | int]:
4    result: dict[str, str | int] = {"id": user_id}
5    if nickname is not None:
6        result["nickname"] = nickname
7    return result

The important part is not the spelling. It is keeping the annotation honest. Writing nickname: str = None is misleading because the default is not actually a str.

Never Use Mutable Defaults for Ordinary Data

This is the classic Python trap:

python
1def add_item(item: str, bucket: list[str] = []) -> list[str]:
2    bucket.append(item)
3    return bucket
4
5print(add_item("a"))
6print(add_item("b"))

That prints a reused list because the default list is created once when the function is defined, not once per call.

The safe pattern is:

python
1def add_item(item: str, bucket: list[str] | None = None) -> list[str]:
2    if bucket is None:
3        bucket = []
4    bucket.append(item)
5    return bucket
6
7print(add_item("a"))
8print(add_item("b"))

This pattern is so common because it is both runtime-safe and type-checker-friendly.

Keyword-Only Defaults Make APIs Safer

When a function has several optional settings, keyword-only parameters improve readability:

python
1def connect(
2    host: str,
3    port: int,
4    *,
5    timeout: float = 5.0,
6    retries: int = 3,
7    use_ssl: bool = True,
8) -> str:
9    return f"{host}:{port} timeout={timeout} retries={retries} ssl={use_ssl}"
10
11print(connect("db.local", 5432))
12print(connect("db.local", 5432, timeout=1.5, retries=1))

This reduces call-site mistakes because the optional arguments must be named explicitly.

When None Means Something Real

Sometimes None is not the same as "argument omitted." In that case, use a sentinel object instead of overloading None with two meanings.

python
1_UNSET = object()
2
3def configure(limit: int | None | object = _UNSET) -> str:
4    if limit is _UNSET:
5        return "using system default"
6    if limit is None:
7        return "limit disabled"
8    return f"limit={limit}"
9
10print(configure())
11print(configure(None))
12print(configure(100))

This is more precise than forcing None to mean both "missing" and "explicitly disabled."

Defaults and Static Type Checkers

Type hints are most useful when tools such as mypy, pyright, or IDE inspections can reason about the function correctly. That means:

  • the default value should be compatible with the annotation
  • return types should reflect real behavior
  • sentinel patterns should be deliberate and documented

For example, this signature is clearer than one that hides optional behavior:

python
1def parse_flag(value: str, default: bool = False) -> bool:
2    cleaned = value.strip().lower()
3    if cleaned in ("true", "1", "yes", "on"):
4        return True
5    if cleaned in ("false", "0", "no", "off"):
6        return False
7    return default

The function always returns bool, and the default stays inside that type.

Common Pitfalls

The biggest mistake is writing a default that does not match the annotation, especially None with a non-optional type.

Another common problem is mutable defaults such as lists and dictionaries. That creates hidden shared state between calls.

Developers also sometimes assume type hints enforce validation at runtime. They do not. If you need runtime checks, add them explicitly or use a validation library.

Finally, keep signatures readable. A technically valid type-hinted function can still be hard to use if too many optional parameters are positional or poorly named.

Summary

  • The normal form is param: Type = default.
  • If the default is None, include None in the type.
  • Avoid mutable defaults such as [] and {}.
  • Use keyword-only defaults when a function has several optional settings.
  • Keep the annotation and the actual runtime behavior aligned so type checkers can help you.

Course illustration
Course illustration

All Rights Reserved.