No history yet

Optional vs Default Values

The Two Meanings of Optional

In Python, the idea of an "optional" value can be confusing. It has two distinct meanings: one for the type system and one for function calls. A variable can be of a type that allows None as a value, and a function argument can be optional to provide. These concepts often overlap but are not the same.

Let's start with the type system. When you see Optional[T] or, in modern Python, T | None, it means a variable is explicitly allowed to hold either a value of type T or the value None. It's a contract with the static type checker, like Mypy.

Optional[T] tells the type checker: "This variable can be a T, or it can be None. Be prepared for both possibilities."

Consider a function that requires a user's middle name. Some people don't have one, so None is a valid piece of data. The argument is required, but its value can be None.

from typing import Optional

def get_full_name(first: str, middle: Optional[str], last: str) -> str:
    if middle:
        return f"{first} {middle} {last}"
    return f"{first} {last}"

# This is valid, the argument is provided as None
print(get_full_name("John", None, "Doe"))

# This is also valid
print(get_full_name("John", "Fitzgerald", "Doe"))

# This is an error, the argument is missing
# get_full_name("John", "Doe")

Default Values

A default value, on the other hand, makes an argument optional at runtime. The caller can choose not to provide it, and Python will substitute the default. This is a feature of the Python language itself, not just the type system.

The common mistake is to assume a default value of None automatically makes the type Optional. A type checker will flag this code:

# Mypy Error: Incompatible default for argument "message"
# (default has type "None", argument has type "str")
def greet(name: str, message: str = None):
    ...

The type hint str promises the message argument will always be a string. But the default value is None, which breaks that promise. To fix this, you must explicitly declare that None is a valid type for the variable. This is where the two concepts merge.

from typing import Optional

# Correct: The argument is optional to provide, and its
# type can be either str or None.
def greet(name: str, message: Optional[str] = None):
# Or using modern syntax:
# def greet(name: str, message: str | None = None):
    if message:
        print(f"{message}, {name}!")
    else:
        print(f"Hello, {name}!")

greet("Alice")
greet("Bob", "Good morning")

Clarifying the Combinations

Understanding the difference comes down to separating two questions:

  1. Is the argument required at call time? (Determined by a default value.)
  2. Can the argument's value be None? (Determined by Optional or | None.)

Here’s a breakdown of the four possible scenarios.

ScenarioSyntaxRequired to Provide?Can Be None?
Required & Not-Nonearg: strYesNo
Required & Can be None`arg: strNone`Yes
Optional & Not-Nonearg: str = "default"NoNo
Optional & Can be None`arg: strNone = None`No

Using Optional[str] instead of just str will let the editor help you detect errors where you could be assuming that a value is always a str, when it could actually be None too.

Type Narrowing with None

The primary benefit of correctly annotating Optional types is enabling type narrowing. A static type checker knows that if a variable's type is str | None, you can't safely call string methods on it—it might be None!

However, once you check if the variable is not None, the type checker is smart enough to "narrow" the type to just str within that code block. This allows you to call string methods safely without errors.

def process_text(text: str | None):
    # Mypy error: Item "None" of "str | None" has no attribute "upper"
    # print(text.upper())

    if text is not None:
        # OK! Inside this block, Mypy knows `text` is a `str`.
        print(text.upper())
    else:
        print("No text provided.")

Distinguishing between Optional types and default values is key to writing robust and clear Python code. Optional manages what values a variable can hold, while default values control whether a function argument must be provided. Using them correctly together creates flexible and type-safe APIs.

Quiz Questions 1/5

What does the type hint str | None (or Optional[str]) indicate to a static type checker?

Quiz Questions 2/5

What is the primary issue with the following function signature from a static typing perspective?

def send_message(message: str = None):
    ...