2025, Oct 19 01:00

PEP 8 Rules for Python Function Parameters: Spacing Defaults with Annotations, Union Types, and Optional

PEP 8 on spacing around = for Python defaults with type annotations: correct style for union types, Optional, and enforcement by Black or Ruff formatters.

Spacing around default values in Python function parameters looks trivial until type annotations enter the picture. The confusion typically spikes with union types, where developers mix and match spaces and parentheses hoping to find the right convention. Let’s settle what PEP 8 actually expects and how to apply it consistently.

Problem setup

There is a widespread belief that defaults in parameter lists shouldn’t have spaces around the equals sign. That impression holds only for unannotated parameters. Once you add an annotation, the rule changes, and with union types the style questions multiply.

def setup(name: str="baz"):
    pass
# Which layout is correct with unions?
def process_payload(token: str | None=None):
    pass
def process_payload(token: str | None = None):
    pass
def process_payload(token: (str | None)=None):
    pass

What actually governs the style

PEP 8 is explicit about the interaction between annotations and default values. The guidance is short and unambiguous:

When combining an argument annotation with a default value, however, do use spaces around the = sign

This single sentence explains why the “no spaces” habit doesn’t apply once a parameter is annotated. The equals sign should be surrounded by spaces in the annotated case, while unannotated defaults stay “glued.” Parentheses around types are not part of the standard style for unions; they are generally unnecessary unless you’re emphasizing grouping. If you prefer a more expressive way to mark an optional value, you can use Optional[...] for simple cases like a single type plus None, though that pattern doesn’t extend to arbitrary unions.

The fix

Applying the PEP 8 recommendation, the function signature with a union type and a default should be written with spaces around the equals sign and without extraneous parentheses.

def process_payload(token: str | None = None):
    pass

If you’re standardizing a larger codebase, a formatter will save time. Tools such as ruff or black will keep spacing consistent and remove the temptation to bikeshed style during reviews.

Why this matters

Consistent annotation and default-value formatting improves readability and reduces ambiguity in signatures. It also pays off in tooling: linters, formatters, and type checkers work best when code adheres to agreed conventions. Small as it seems, uniform spacing prevents style drift and makes diffs cleaner.

Takeaways

Remember the simple split: annotated parameters use spaces around the equals sign, unannotated ones do not. Avoid parenthesizing union types unless you intentionally want extra grouping. For optional single-type parameters, Optional[...] may read more clearly than T | None in some contexts, while broader unions stay with the | form. Adopt an auto-formatter to lock in these choices and keep the team focused on logic rather than layout.

The article is based on a question from StackOverflow by GrandeKnight and an answer by Mureinik.