If your code is designed to work with Python 3.10 or newer, you want to use the PEP 604 syntax, using ... | None union syntax, and not use typing.Optional:

def test(a: dict[Any, Any] | None = None) -> None:
    #print(a) ==> {'a': 1234}
    #or
    #print(a) ==> None

def test(a: list[Any] | None = None) -> None:
    #print(a) ==> [1, 2, 3, 4, 'a', 'b']
    #or
    #print(a) ==> None

Code that still supports older Python versions can still stick to using Optional. Optional[...] is a shorthand notation for Union[..., None], telling the type checker that either an object of the specific type is required, or None is required. ... stands for any valid type hint, including complex compound types or a Union[] of more types. Whenever you have a keyword argument with default value None, you should use Optional.

So for your two examples, you have dict and list container types, but the default value for the a keyword argument shows that None is permitted too so use Optional[...]:

from typing import Optional

def test(a: Optional[dict] = None) -> None:
    #print(a) ==> {'a': 1234}
    #or
    #print(a) ==> None

def test(a: Optional[list] = None) -> None:
    #print(a) ==> [1, 2, 3, 4, 'a', 'b']
    #or
    #print(a) ==> None

There is technically no difference between using Optional[] on a Union[], or just adding None to the Union[]. So Optional[Union[str, int]] and Union[str, int, None] are exactly the same thing.

Personally, I'd stick with always using Optional[] when setting the type for a keyword argument that uses = None to set a default value, this documents the reason why None is allowed better. Moreover, it makes it easier to move the Union[...] part into a separate type alias, or to later remove the Optional[...] part if an argument becomes mandatory.

For example, say you have

from typing import Optional, Union

def api_function(optional_argument: Optional[Union[str, int]] = None) -> None:
    """Frob the fooznar.

    If optional_argument is given, it must be an id of the fooznar subwidget
    to filter on. The id should be a string, or for backwards compatibility,
    an integer is also accepted.

    """

then documentation is improved by pulling out the Union[str, int] into a type alias:

from typing import Optional, Union

# subwidget ids used to be integers, now they are strings. Support both.
SubWidgetId = Union[str, int]


def api_function(optional_argument: Optional[SubWidgetId] = None) -> None:
    """Frob the fooznar.

    If optional_argument is given, it must be an id of the fooznar subwidget
    to filter on. The id should be a string, or for backwards compatibility,
    an integer is also accepted.

    """

The refactor to move the Union[] into an alias was made all the much easier because Optional[...] was used instead of Union[str, int, None]. The None value is not a 'subwidget id' after all, it's not part of the value, None is meant to flag the absence of a value.

Side note: Unless your code only has to support Python 3.9 or newer, you want to avoid using the standard library container types in type hinting, as you can't say anything about what types they must contain. So instead of dict and list, use typing.Dict and typing.List, respectively. And when only reading from a container type, you may just as well accept any immutable abstract container type; lists and tuples are Sequence objects, while dict is a Mapping type:

from typing import Mapping, Optional, Sequence, Union

def test(a: Optional[Mapping[str, int]] = None) -> None:
    """accepts an optional map with string keys and integer values"""
    # print(a) ==> {'a': 1234}
    # or
    # print(a) ==> None

def test(a: Optional[Sequence[Union[int, str]]] = None) -> None:
    """accepts an optional sequence of integers and strings
    # print(a) ==> [1, 2, 3, 4, 'a', 'b']
    # or
    # print(a) ==> None

In Python 3.9 and up, the standard container types have all been updated to support using them in type hints, see PEP 585. But, while you now can use dict[str, int] or list[Union[int, str]], you still may want to use the more expressive Mapping and Sequence annotations to indicate that a function won't be mutating the contents (they are treated as 'read only'), and that the functions would work with any object that works as a mapping or sequence, respectively.

Python 3.10 introduces the | union operator into type hinting, see PEP 604. Instead of Union[str, int] you can write str | int. In line with other type-hinted languages, the preferred (and more concise) way to denote an optional argument in Python 3.10 and up, is now Type | None, e.g. str | None or list | None.

Answer from Martijn Pieters on Stack Overflow
Top answer
1 of 5
642

If your code is designed to work with Python 3.10 or newer, you want to use the PEP 604 syntax, using ... | None union syntax, and not use typing.Optional:

def test(a: dict[Any, Any] | None = None) -> None:
    #print(a) ==> {'a': 1234}
    #or
    #print(a) ==> None

def test(a: list[Any] | None = None) -> None:
    #print(a) ==> [1, 2, 3, 4, 'a', 'b']
    #or
    #print(a) ==> None

Code that still supports older Python versions can still stick to using Optional. Optional[...] is a shorthand notation for Union[..., None], telling the type checker that either an object of the specific type is required, or None is required. ... stands for any valid type hint, including complex compound types or a Union[] of more types. Whenever you have a keyword argument with default value None, you should use Optional.

So for your two examples, you have dict and list container types, but the default value for the a keyword argument shows that None is permitted too so use Optional[...]:

from typing import Optional

def test(a: Optional[dict] = None) -> None:
    #print(a) ==> {'a': 1234}
    #or
    #print(a) ==> None

def test(a: Optional[list] = None) -> None:
    #print(a) ==> [1, 2, 3, 4, 'a', 'b']
    #or
    #print(a) ==> None

There is technically no difference between using Optional[] on a Union[], or just adding None to the Union[]. So Optional[Union[str, int]] and Union[str, int, None] are exactly the same thing.

Personally, I'd stick with always using Optional[] when setting the type for a keyword argument that uses = None to set a default value, this documents the reason why None is allowed better. Moreover, it makes it easier to move the Union[...] part into a separate type alias, or to later remove the Optional[...] part if an argument becomes mandatory.

For example, say you have

from typing import Optional, Union

def api_function(optional_argument: Optional[Union[str, int]] = None) -> None:
    """Frob the fooznar.

    If optional_argument is given, it must be an id of the fooznar subwidget
    to filter on. The id should be a string, or for backwards compatibility,
    an integer is also accepted.

    """

then documentation is improved by pulling out the Union[str, int] into a type alias:

from typing import Optional, Union

# subwidget ids used to be integers, now they are strings. Support both.
SubWidgetId = Union[str, int]


def api_function(optional_argument: Optional[SubWidgetId] = None) -> None:
    """Frob the fooznar.

    If optional_argument is given, it must be an id of the fooznar subwidget
    to filter on. The id should be a string, or for backwards compatibility,
    an integer is also accepted.

    """

The refactor to move the Union[] into an alias was made all the much easier because Optional[...] was used instead of Union[str, int, None]. The None value is not a 'subwidget id' after all, it's not part of the value, None is meant to flag the absence of a value.

Side note: Unless your code only has to support Python 3.9 or newer, you want to avoid using the standard library container types in type hinting, as you can't say anything about what types they must contain. So instead of dict and list, use typing.Dict and typing.List, respectively. And when only reading from a container type, you may just as well accept any immutable abstract container type; lists and tuples are Sequence objects, while dict is a Mapping type:

from typing import Mapping, Optional, Sequence, Union

def test(a: Optional[Mapping[str, int]] = None) -> None:
    """accepts an optional map with string keys and integer values"""
    # print(a) ==> {'a': 1234}
    # or
    # print(a) ==> None

def test(a: Optional[Sequence[Union[int, str]]] = None) -> None:
    """accepts an optional sequence of integers and strings
    # print(a) ==> [1, 2, 3, 4, 'a', 'b']
    # or
    # print(a) ==> None

In Python 3.9 and up, the standard container types have all been updated to support using them in type hints, see PEP 585. But, while you now can use dict[str, int] or list[Union[int, str]], you still may want to use the more expressive Mapping and Sequence annotations to indicate that a function won't be mutating the contents (they are treated as 'read only'), and that the functions would work with any object that works as a mapping or sequence, respectively.

Python 3.10 introduces the | union operator into type hinting, see PEP 604. Instead of Union[str, int] you can write str | int. In line with other type-hinted languages, the preferred (and more concise) way to denote an optional argument in Python 3.10 and up, is now Type | None, e.g. str | None or list | None.

2 of 5
91

Directly from mypy typing module docs.

Optional[str] is just a shorthand or alias for Union[str, None]. It exists mostly as a convenience to help function signatures look a little cleaner.

Update for Python 3.10+

you can now use the pipe operator as well.

# Python < 3.10
def get_cars(size: Optional[str]=None):
    pass
# Python 3.10+
def get_cars(size: str|None=None):
    pass
🌐
Real Python
realpython.com › python-optional-arguments
Using Python Optional Arguments When Defining Functions – Real Python
October 27, 2025 - This tutorial shows you how and why to use Python optional arguments, and how to avoid common pitfalls when setting defaults. By the end of this tutorial, you’ll understand that: Parameters are names in a function definition, while arguments are the values you pass when calling the function · You can assign default values to parameters so that arguments become optional · You should avoid mutable data types like lists or dictionaries as default values to prevent unexpected behavior
Discussions

python - How do I define a function with optional arguments? - Stack Overflow
I have a Python function which takes several arguments. Some of these arguments could be omitted in some scenarios. Copydef some_function (self, a, b, c, d = None, e = None, f = None, g = None, h = None): #code · The arguments d through h are strings which each have different meanings. It is important that I can choose which optional ... More on stackoverflow.com
🌐 stackoverflow.com
Type hinting with for callables with optional arguments
Currently on a train but in short you want to take a look at protocols. They are available since Python 3.8+. PEP: https://peps.python.org/pep-0544/ More on reddit.com
🌐 r/learnpython
9
2
May 30, 2024
Adding optional arguments to a function
Type hints are ignored by the interpreter. You still need to provide a default value for the argument if one isn't provided in the function call. The appropriate default value to use with the Optional type hint is None, so: def function( data1: list, data2: list, opt1: Optional[list] = None, new: Optional[dict] = None, ) More on reddit.com
🌐 r/learnpython
2
1
September 8, 2021
python - Optional argument type annotation - Stack Overflow
If we want a function that takes an optional argument x, we'll commonly do this: def f(x: int | None = None) -> None: if x is None: print("No value was given.") else: ... More on stackoverflow.com
🌐 stackoverflow.com
🌐
Mimo
mimo.org › glossary › python › optional-arguments
Python Optional Argument: Syntax, Usage, and Examples
In a Python function, an optional argument is often called a default argument, because the value is already there unless you override it. ... Become a Python developer. Master Python from basics to advanced topics, including data structures, functions, classes, and error handling ...
🌐
Python documentation
docs.python.org › 3 › library › typing.html
typing — Support for type hints
On the other hand, if an explicit value of None is allowed, the use of Optional is appropriate, whether the argument is optional or not. For example: ... Changed in version 3.10: Optional can now be written as X | None. See union type expressions.
🌐
Towards Data Science
towardsdatascience.com › home › latest › python types: optional can mean mandatory
Python Types: Optional Can Mean Mandatory | Towards Data Science
January 29, 2025 - Mypy has powerful type inference that lets you use regular Python idioms to guard against None values. ... In this option, what you really need is a default value of n, but n cannot be None. Here, you simply don’t have to provide the value of n, because, with a default value, you don’t have to provide it’s value in a call to foo(). So, this is the correct meaning of optional in English (the argument is optional because you don’t have to provide it’s value) but incorrect in the typing syntax (the argument is not Optional because n cannot be None).
🌐
Medium
medium.com › @laurentkubaski › python-type-hints-how-many-ways-can-you-say-optional-a940f7ef03e2
Python Type Hints: How Many Ways Can You Say ‘Optional’? | by Laurent Kubaski | Medium
September 2, 2025 - Union[str, None] # This means the parameter doesn't have to be sent Optional[str] # this means the parameter should be sent, but can be None · Yes, this is incorrect: those 2 type hints actually mean the same thing (ie: “The parameter can be either a string or ‘None’, but a value needs to be sent”).
Find elsewhere
🌐
Reddit
reddit.com › r/learnpython › type hinting with for callables with optional arguments
r/learnpython on Reddit: Type hinting with for callables with optional arguments
May 30, 2024 -

Hello, everyone.

I am working on a python script that has a specific function that takes as its first argument another callable (called metrics) with signatures like the following:

def metric(mos: Optional["MOS_CS"] = None,
           Vg: Optional[float] = None,
           Vd: Optional[float] = None,
           f: Optional[float] = None,
           T: Optional[float] = None,
           ) -> complex:

The way I've annotated those metrics in the function's signature is the following:

Callable[[Optional["MOS_CS"], Optional[float], Optional[float], Optional[float], Optional[float]], complex]

where MOS_CS is a custom class.

The problem I'm having is that different metrics may have some of those parameters that are not optional, such as the following:

def other_metric(mos: Optional["MOS_CS"] = None,
                 Vg: float,
                 f: float,
                 Vd: Optional[float] = None,
                 T: Optional[float] = None,
                 ) -> complex:

The thing is, every metric should accept those 4 parameters no matter what, but not all metrics will use them, and when not used, they should be equal to None.

I tried passing the following metric to my function:

def S12(mos: "MOS_CS",
            Vg: float,
            Vd: float,
            f: float,
            T: float,
            ) -> complex:

This metric needs all 4 parameters. But mypy gives me the following error:

Argument "metric" to "metric_wrt_f" of "MOS_CS" has incompatible type "Callable[[MOS_CS, float, float, float, float], complex]"; expected "Callable[[MOS_CS | None, float | None, float | None, float | None, float | None], complex]"

There are 16 possible combinations of having the parameters optional or not. How can I annotate my function such that it accepts all 16 possibilities without doing a big union between them all?

EDIT: Forgot to write that all parameters are always passed by name.

EDIT 2: Following the suggestion from u/speedy19981, I've looked into protocols and realised I would be better served by a callback protocol instead of the regular type annotation. But my problem kinda persists.

My callback protocol is the following:

class Metric(Protocol):
    def __call__(self,
                 *,
                 mos: Optional["MOS_CS"] = None,
                 Vg: Optional[float] = None,
                 Vd: Optional[float] = None,
                 f: Optional[float] = None,
                 T: Optional[float] = None,
                 ) -> complex:

        ...

I've added the asterisk to enforce the keyword-only argument passing I mentioned on my previous edit.

The metric metric shown previously on the text is compliant with the protocol Metric but the metric other_metric is not. I've tried making various callback protocols for each of the 16 possibilities and then create a super protocol that inherits all other protocols, but since each of the 16 protocols has a __call__ method with different signatures, it throws an error.

I could make 16 protocols Metric0 up to Metric15 and then create a type alias using unions like Metric0 | Metric1 ... Metric14 | Metric15, but that sounds un-pythonic to me. Ideally, I would have all that complexity hidden away inside the protocol class. Is it possible?

EDIT 3: I'm going to have all metrics with the same signature and make the check for the parameters I want to be mandatory inside each metric individually.

🌐
Reddit
reddit.com › r/learnpython › adding optional arguments to a function
r/learnpython on Reddit: Adding optional arguments to a function
September 8, 2021 -

I am working on a project where I'm supposed to add new features to an existing codebase. As part of this, I need to add an optional argument to one of the functions but just adding the optional argument is causing some of my unit tests to fail.

The function looks like the following initially:

def function(data1: list,     
            data2: list,
             opt1: Optional[list],
 ) 

After adding another optional argument it looks like this:

def function(
    data1: list,
    data2: list,
    opt1: Optional[list],
    new: Optional[dict],
)

The only change I'm making in the codebase is adding this optional argument and it is causing some of my unit tests to fail. I was wondering if someone knows what might be the reason ?

🌐
GeeksforGeeks
geeksforgeeks.org › python › how-to-pass-optional-parameters-to-a-function-in-python
How to Pass Optional Parameters to a Function in Python - GeeksforGeeks
July 23, 2025 - In Python, functions can have optional parameters by assigning default values to some arguments. This allows users to call the function with or without those parameters, making the function more flexible.
🌐
Index.dev
index.dev › blog › python-functions-optional-arguments
5 Ways to Define a Function with Python Optional Arguments
November 4, 2024 - Learn five practical approaches to defining functions with optional arguments in Python, including default values, flexible parameters and function annotations.
🌐
The Python Coding Book
thepythoncodingbook.com › home › blog › optional arguments with default values in python functions [intermediate python functions series #3]
Optional Arguments with Default Values in Python Functions
January 18, 2023 - When you call the function, the corresponding argument is optional. If the argument is not present in the function call, the default value is used · Next Article: Argh! What are args and kwargs in Python?
🌐
Leapcell
leapcell.io › blog › understanding-optional-arguments-in-python
Understanding Optional Arguments in Python | Leapcell
July 25, 2025 - Optional arguments are function parameters that are not required when the function is called. They are defined with default values in the function signature. If no value is passed for an optional argument, the default value is used.
🌐
Nelson
nelson.cloud › posts › optional type hints in python
Optional Type Hints in Python | Nelson Figueroa
April 14, 2026 - As of Python 3.10, you can also use a shorthand notation, replacing Optional[str] with str | None. There is no import required: ... Both examples above are equivalent to each other. Both functions also accept None as a return type. Here are some useful examples showing optional type hints using the typing module and the shorthand notation. A single optional type hint for an integer as an argument ...
🌐
University of Toronto
teach.cs.toronto.edu › ~csc110y › fall › notes › 12-interlude-nifty-python-features › 03-functions-with-optional-parameters.html
12.3 Functions with Optional Parameters
# parameter definition with default value syntax def ...(<parameter_name>: <parameter_type> = <default_value>, ...) -> ...: Let’s see an example of this. Suppose we want to define a function that takes a number n and by default returns n + 1, but allows the caller to specify an optional step amount to increase by. def increment(n: int, step: int = 1) -> int: """Return n incremented by step. If the step argument is omitted, increment by 1 instead. """ return n + step · Let’s experiment with this function in the Python console:
🌐
GoLinuxCloud
golinuxcloud.com › home › programming › master python optional arguments usage [tutorial]
Master Python Optional Arguments Usage [Tutorial] | GoLinuxCloud
January 9, 2024 - When it comes to function definitions in Python, the general rule for combining various types of arguments is: ... In this example, arg1 and arg2 are positional arguments, *args catches additional optional positional arguments, kwarg1 is a keyword argument with a default value, and **kwargs captures additional optional keyword arguments.
🌐
iO Flood
ioflood.com › blog › python-optional-arguments
Python Optional Arguments | Guide (With Examples)
November 17, 2023 - def greet(greeting, name='World'): print(f'{greeting}, {name}!') greet() # Output: # TypeError: greet() missing 1 required positional argument: 'greeting' In this example, we forgot to provide the required positional argument greeting when calling the function greet(). To fix this, make sure to provide all required arguments in your function calls. Remember, Python optional arguments can make your code more flexible and powerful, but they require careful handling.
🌐
Stack Overflow
stackoverflow.com › questions › 78299678 › optional-argument-type-annotation
python - Optional argument type annotation - Stack Overflow
If we want a function that takes an optional argument x, we'll commonly do this: def f(x: int | None = None) -> None: if x is None: print("No value was given.") else: ...
🌐
Medium
medium.com › @khanfarazahmed7 › difference-between-required-optional-positional-and-keyword-arguments-in-python-functions-1fa9cd6a46c4
Difference between Required, Optional, Positional and Keyword arguments in Python functions | by Faraz Khan | Medium
November 15, 2023 - Optional Arguments: These are more like the laid-back attendees who can join if they want. You can make an ordinary argument optional by giving it a default value during function creation.