Unlike statically-typed languages such as C or Java, Python does not have variable declarations. Variables come into existence only when you assign a value to them. There is no syntax for declaring a variable without simultaneously giving it a value. However, Python offers several idiomatic patterns for creating placeholder variables when the actual value is not yet known.
In this guide, you will learn the most common approaches for initializing variables without meaningful values, understand when to use each pattern, and avoid common anti-patterns that can introduce subtle bugs.
Using None as a Placeholder
None is Python's built-in null value, representing the intentional absence of data. It is the most common and idiomatic way to signal that a variable exists but does not yet hold a meaningful value:
user = None
result = None
connection = None
if user is None:
print("No user logged in")Output:
No user logged inThe variable is assigned a real value later when the data becomes available:
user = None
user = {"name": "Alice", "role": "admin"}
if user is not None:
print(f"Logged in as {user['name']}")Output:
Logged in as AliceType Hints with Initial Values
Modern Python supports type annotations that document what type a variable will eventually hold. However, the annotation alone does not create the variable. You still need an assignment:
name: str | None = None
count: int = 0
items: list[str] = []
print(name)
print(count)
print(items)Output:
None
0
[]For Python 3.9 and earlier, use the typing module for the same effect:
from typing import Optional, List
name: Optional[str] = None
items: List[str] = []Empty Containers
When you know the variable will hold a collection that gets populated over time, initialize it as an empty container:
results = []
results.append("first")
results.append("second")
print(results)
cache = {}
cache["key"] = "value"
print(cache)
seen = set()
seen.add("item")
seen.add("item") # Duplicate ignored
print(seen)Output:
['first', 'second']
{'key': 'value'}
{'item'}This is cleaner than initializing with None and then replacing it with a container later, because you avoid needing to check whether the variable is None before calling methods like .append().
Zero and Empty String Defaults
When the variable has a known type and a neutral starting value makes semantic sense, use the type's natural zero or empty value:
total = 0
name = ""
is_active = False
total += 10
total += 20
print(f"Total: {total}")
name = "Alice"
print(f"Name: {name}")Output:
Total: 30
Name: AliceThis approach works well for counters, accumulators, and flags where the initial state has a clear meaning.
Sentinel Values
Sometimes None is a legitimate value in your domain, and you need a separate way to distinguish "not yet set" from "explicitly set to None." A unique sentinel object solves this:
_UNSET = object() # Unique sentinel that is not None
def get_config(key, default=_UNSET):
config = {"debug": True, "timeout": None}
value = config.get(key, _UNSET)
if value is _UNSET:
if default is _UNSET:
raise KeyError(f"Missing required config: {key}")
return default
return value
print(get_config("debug"))
print(get_config("timeout"))
print(get_config("missing", default="fallback"))Output:
True
None
fallbackThe sentinel _UNSET is a unique object that can never be confused with None, 0, False, or any other valid value. This pattern is common in library code where None might be a meaningful return value.
Class Attributes
In classes, declare instance variables in __init__ with placeholder values:
class User:
def __init__(self):
self.name: str | None = None
self.email: str | None = None
self.age: int = 0
self.roles: list[str] = []
def setup(self, name: str, email: str):
self.name = name
self.email = email
user = User()
print(f"Before setup: name={user.name}, roles={user.roles}")
user.setup("Alice", "alice@example.com")
print(f"After setup: name={user.name}, email={user.email}")Output:
Before setup: name=None, roles=[]
After setup: name=Alice, email=alice@example.comFor a more concise approach, use dataclasses with default values:
from dataclasses import dataclass, field
@dataclass
class User:
name: str | None = None
email: str | None = None
age: int = 0
roles: list[str] = field(default_factory=list)
user = User(name="Alice")
print(user)Output:
User(name='Alice', email=None, age=0, roles=[])The Ellipsis for Stubs and Abstract Methods
The ellipsis literal (...) serves as a placeholder in abstract methods, type stubs, and functions that are intentionally left unimplemented:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
@abstractmethod
def perimeter(self) -> float:
...
def not_implemented_yet():
... # Equivalent to pass, signals intentional emptinessThe ellipsis communicates intent more clearly than pass in these contexts: it tells other developers that the body is deliberately empty, not accidentally forgotten.
Common Anti-Patterns to Avoid
Accessing a Variable That Was Never Assigned
print(undefined_variable)Output:
NameError: name 'undefined_variable' is not definedMutable Default Arguments
A particularly common and dangerous mistake is using a mutable object as a default argument value:
def bad_default(items=[]):
items.append(1)
return items
print(bad_default())
print(bad_default())
print(bad_default())Output:
[1]
[1, 1]
[1, 1, 1]The same list object is shared across all calls to the function, causing items to accumulate unexpectedly. The correct pattern uses None and creates a fresh list inside the function body:
def good_default(items=None):
if items is None:
items = []
items.append(1)
return items
print(good_default())
print(good_default())
print(good_default())Output:
[1]
[1]
[1]Summary
| Pattern | Syntax | Use Case |
|---|---|---|
| None placeholder | x = None |
Unknown value, optional data |
| Type-hinted None | x: int | None = None |
Documented optional values |
| Empty container | x = [] or x = {} |
Collections to populate |
| Zero or empty default | x = 0 or x = "" |
Known type with neutral value |
| Sentinel object | x = object() |
When None is a valid value |
| Ellipsis | ... |
Stubs and abstract methods |
Conclusion
Python's philosophy is explicit initialization over implicit declaration.
- There is no way to declare a variable without giving it a value, but you can use meaningful placeholders that clearly communicate intent.
- Choose
Nonefor most cases where a value is not yet available. Use empty containers when you will be accumulating items. - Use typed defaults like
0,"", orFalsewhen a neutral value makes semantic sense.
And reserve sentinel objects for the rare cases where None itself is a valid data value that you need to distinguish from "not set."