Of course! The term "union object" in Python most commonly refers to a Union type hint. It allows you to specify that a variable, function parameter, or return value can be one of several different types.

This is a core feature of Python's Type Hinting system, introduced in PEP 484 and significantly enhanced with the introduction of the typing module's special features like the pipe operator () in Python 3.10.
Let's break it down.
The Union Type from the typing Module
Before Python 3.10, the standard way to create a union was to import Union from the typing module and pass the possible types as arguments.
Syntax
from typing import Union variable_name: Union[Type1, Type2, Type3]
Example
Imagine you have a function that can process either a string or a list of integers.

from typing import Union, List
def process_data(data: Union[str, List[int]]) -> str:
"""
Processes data, which can be either a string or a list of integers.
Returns a formatted string.
"""
if isinstance(data, str):
return f"Processed string: {data.upper()}"
elif isinstance(data, list):
# We can use a type guard here to satisfy type checkers
return f"Processed list: Sum is {sum(data)}"
else:
# This case should ideally not be reached if the type hint is correct
raise TypeError("Invalid data type")
# --- Usage ---
# These calls are valid
str_result = process_data("hello world")
print(str_result)
# Output: Processed string: HELLO WORLD
list_result = process_data([1, 2, 3, 4, 5])
print(list_result)
# Output: Processed list: Sum is 15
# This call would cause a TypeError at runtime if not handled,
# and a type checker like MyPy would flag it as an error.
# process_data(123)
Why use Union?
- Clarity: It makes your code's expected inputs and outputs explicit.
- Static Type Checkers: Tools like MyPy, Pyright, and PyCharm's linter can analyze your code and catch potential type errors before you even run the script. This is invaluable for large, complex projects.
The Modern Way: The Pipe Operator (Python 3.10+)
PEP 604 introduced a more concise and readable syntax for type unions using the pipe operator (), which is the same operator used for bitwise OR. This is now the preferred method if you are using Python 3.10 or newer.
Syntax
variable_name: Type1 | Type2 | Type3
This is functionally identical to Union[Type1, Type2, Type3] but is much cleaner.
Example
Let's rewrite the previous example using the new syntax.

# No need to import anything for this syntax in Python 3.10+
def process_data_modern(data: str | list[int]) -> str:
"""
The same function, but with modern type hints.
Note: list[int] is also a newer syntax (from PEP 585).
"""
if isinstance(data, str):
return f"Processed string: {data.title()}"
elif isinstance(data, list):
return f"Processed list: Average is {sum(data) / len(data):.2f}"
else:
raise TypeError("Invalid data type")
# --- Usage ---
str_result = process_data_modern("python is awesome")
print(str_result)
# Output: Processed string: Python Is Awesome
list_result = process_data_modern([10, 20, 30])
print(list_result)
# Output: Processed list: Average is 20.00
Note on list[int]: The example above also uses list[int], which is another modern feature (PEP 585) that allows using built-in types like list, dict, and tuple directly as generics, instead of having to import List from typing.
Practical Use Cases
Unions are incredibly useful in many real-world scenarios.
a) Handling Optional Values
A very common use case is to represent a value that can be of a certain type or None. Before Python 3.10, you had to write Union[Type, None]. Now, there's a dedicated shorthand for this: Type | None.
This is often called the "Optional" type.
# Before Python 3.10
from typing import Union, Optional
def get_user_name(user_id: int) -> Union[str, None]:
# ... logic to fetch name from a database ...
if user_exists:
return "Alice"
return None
# Python 3.10+ (Preferred)
def get_user_name_modern(user_id: int) -> str | None:
# ... same logic ...
if user_exists:
return "Bob"
return None
# The Optional[T] type from the typing module is an alias for Union[T, None]
# So, this is also valid and common for compatibility:
from typing import Optional
def get_user_name_optional(user_id: int) -> Optional[str]:
# ... same logic ...
return "Charlie" if user_exists else None
b) Dealing with API Responses or Configuration Files
Data from external sources (like JSON APIs or config files) often doesn't have a single, rigid structure. A field might be a string or a number.
import json
from typing import Union
# A configuration dictionary where a timeout can be an int or a string
config_data = {
"timeout": 30,
"retries": "3",
"verbose": True
}
def get_timeout(config: dict) -> int | str:
"""Safely retrieves the timeout value, which can be an int or str."""
return config.get("timeout", 10)
timeout_value = get_timeout(config_data)
print(f"The timeout is: {timeout_value} (type: {type(timeout_value)})")
# Output: The timeout is: 30 (type: <class 'int'>)
config_data_str_timeout = {"timeout": "45"}
timeout_value_str = get_timeout(config_data_str_timeout)
print(f"The timeout is: {timeout_value_str} (type: {type(timeout_value_str)})")
# Output: The timeout is: 45 (type: <class 'str'>)
Advanced: Union with TypeVar and Literal
For more complex scenarios, you can combine Union with other typing features.
Union with Literal
You can use Union to specify that a value must be one of a specific set of literal values. This is great for enums or status codes.
from typing import Union, Literal
def set_status(status: Union["active", "inactive", "pending"]) -> None:
print(f"Setting status to: {status}")
set_status("active")
# set_status("approved") # MyPy would flag this as an error!
Union with TypeVar and bound
This is more advanced but powerful. You can create a generic function that works with a union of types but shares a common base or interface.
from typing import TypeVar, Union
# Define a base class
class Animal:
def speak(self) -> str:
raise NotImplementedError
# Define some subclasses
class Dog(Animal):
def speak(self) -> str:
return "Woof!"
class Cat(Animal):
def speak(self) -> str:
return "Meow!"
# Create a TypeVar that is bound to the Animal base class
T = TypeVar('T', bound=Animal)
# The function accepts any object that is a Dog OR a Cat
# The 'bound=Animal' ensures they both have a .speak() method
def let_animal_speak(animal: Dog | Cat) -> str:
return animal.speak()
# This works
my_dog = Dog()
my_cat = Cat()
print(let_animal_speak(my_dog)) # Output: Woof!
print(let_animal_speak(my_cat)) # Output: Meow!
Summary and Best Practices
| Feature | Python 3.9 & Older (Legacy) | Python 3.10+ (Modern) |
|---|---|---|
| Union Syntax | from typing import Unionmy_var: Union[int, str] |
my_var: int \| str |
| Optional Syntax | from typing import Optionalmy_var: Optional[int] (or Union[int, None]) |
my_var: int \| None |
| Generic Syntax | from typing import List, Dictmy_list: List[int] |
my_list: list[int]my_dict: dict[str, int] |
Best Practices:
- Use the Modern Syntax (Python 3.10+): Always prefer the pipe operator () and built-in generics (
list,dict) for cleaner, more readable code. - Use
Optionalor| Nonefor Nullable Values: It's more idiomatic and clearly expresses the intent that a value can be absent. - Combine with
isinstancefor Runtime Logic: Type hints are primarily for static analysis. At runtime, you still need to useisinstance()to check the type of an object before performing operations specific to that type. - Use a Type Checker: Don't just write type hints—use a tool like MyPy to catch errors in your CI/CD pipeline or as part of your development workflow. This is where type hints provide their greatest value.
