Skip to main content

How to Declare a Variable Without a Value in Python

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

# Check for uninitialized state
if user is None:
print("No user logged in")

Output:

No user logged in

The variable is assigned a real value later when the data becomes available:

user = None

# ... later in the program
user = {"name": "Alice", "role": "admin"}

if user is not None:
print(f"Logged in as {user['name']}")

Output:

Logged in as Alice
tip

Always use is None and is not None for None checks rather than == None. The is operator checks identity, which is both faster and more correct since None is a singleton object.

Type 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:

# Type hint with None placeholder
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] = []
note

A type annotation without assignment, such as name: str, is valid syntax but does not create a usable variable. It only registers a type hint in the module's __annotations__ dictionary. Attempting to access name afterward raises a NameError:

name: str
print(name)

Output:

NameError: name 'name' is not defined

Empty Containers

When you know the variable will hold a collection that gets populated over time, initialize it as an empty container:

# Empty list for accumulating items
results = []
results.append("first")
results.append("second")
print(results)

# Empty dictionary for key-value pairs
cache = {}
cache["key"] = "value"
print(cache)

# Empty set for unique items
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: Alice

This 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
fallback

The 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.com

For 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 emptiness

The 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

# This raises NameError because the variable does not exist
print(undefined_variable)

Output:

NameError: name 'undefined_variable' is not defined

Mutable 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]
warning

Never use mutable objects (lists, dictionaries, sets) as default argument values. Use None as the default and create the object inside the function body. This is one of the most common sources of subtle bugs in Python.

Summary

PatternSyntaxUse Case
None placeholderx = NoneUnknown value, optional data
Type-hinted Nonex: int | None = NoneDocumented optional values
Empty containerx = [] or x = {}Collections to populate
Zero or empty defaultx = 0 or x = ""Known type with neutral value
Sentinel objectx = 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 None for most cases where a value is not yet available. Use empty containers when you will be accumulating items.
  • Use typed defaults like 0, "", or False when 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."