None is a fundamental concept in Python, representing the absence of a
value or a null value. It is an object of its own type, NoneType, and
there is exactly one instance of it in the entire Python interpreter (a
singleton). Understanding how to properly handle None is crucial for writing
robust, bug-free code.
In this tutorial we cover everything you need to know about None:
- What
Noneis, its history, and how it behaves internally. - How
Nonecompares to null/nil in other languages. - How to check for
Nonecorrectly. - Common pitfalls and how to avoid them.
- Best practices for using
Nonein functions, collections, and classes. - Type hints and
Optionaltyping. - Advanced patterns: sentinels, None-aware operators, pattern matching, and more.
The concept of null references was introduced by Tony Hoare in 1965 while designing ALGOL W. In 2009 he famously called it his "billion-dollar mistake":
"I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years."
Languages like C, Java, and JavaScript all inherited some form of null reference, each with its own failure mode:
| Language | Null concept | Common failure mode |
|---|---|---|
| C | NULL (zero pointer) |
Segmentation fault on dereference |
| Java | null |
NullPointerException at runtime |
| JavaScript | null and undefined |
TypeError: Cannot read property of null |
| C# | null |
NullReferenceException |
| SQL | NULL |
Three‑value logic surprises |
| Python | None |
AttributeError: 'NoneType' object has no attribute ... |
Python's design philosophy embraces None as a first-class, explicit,
documented value rather than a source of undefined behavior. You cannot
accidentally get a dangling null pointer — at worst you get a clear
AttributeError pointing exactly to the problematic line.
-
Python 1.x (early 1990s):
Noneexisted from the very beginning of Python. Guido van Rossum included it as the canonical "no value" object, borrowing from the null-object idea while giving it a proper type. -
Python 2.x:
Nonewas a built-in name but could technically be re-assigned at the module level (None = 1), which was a significant footgun. Linters warned about it, but the interpreter itself allowed it. -
Python 2.4:
Nonebecame a protected built-in constant. The interpreter started issuingSyntaxWarningwhen code tried to assign to it. -
Python 3.0 (2008):
None(alongsideTrueandFalse) became a reserved keyword. Assigning to it is now always aSyntaxError:None = 42 # SyntaxError: cannot assign to None
-
Python 3.5 (2015):
typing.Optional[T]was introduced, giving static analysis tools a first-class way to track "could beNone". -
Python 3.10 (2021): Structural Pattern Matching (
match/case) added support for matchingNoneas a literal pattern, making None-handling cleaner in complex dispatch logic. -
Python 3.10+: The
T | Noneunion syntax (int | None) became legal, replacing the more verboseOptional[int]in many situations.
None is implemented in CPython as a C-level singleton (Py_None), a global
object of type PyObject with the special type NoneType. Key facts:
- Immutable — you cannot change its value or add attributes.
- Singleton — the interpreter guarantees exactly one instance exists.
id(None)returns the same memory address everywhere in the same process. - Reference-counted but never freed — the reference count of
Noneis kept artificially high so it is never garbage-collected. - Falsy —
bool(None)returnsFalse; it evaluates toFalsein any boolean context, but it is not equal toFalse.
print(type(None)) # <class 'NoneType'>
print(None is None) # True — singleton guarantee
print(bool(None)) # False — falsy
print(None == False) # False — not equal to False
print(None == 0) # False — not equal to zero
print(id(None)) # same address every time, e.g. 9756672
print(id(None) == id(None)) # True — same objectYou can access the type programmatically:
NoneType = type(None)
print(NoneType) # <class 'NoneType'>
print(isinstance(None, NoneType)) # TrueNoneType is not exposed in any standard namespace — type(None) is the only
way to get it without third-party libraries.
JavaScript has two "nothing" values, a notorious source of bugs:
let x; // x is undefined — variable exists but unassigned
let y = null; // y is null — explicitly "no value"
typeof undefined // "undefined"
typeof null // "object" — famous JS bug
null == undefined // true (loose equality)
null === undefined // false (strict equality)Python's None maps most closely to JavaScript's null. Python has no
undefined; accessing an undefined name raises NameError.
Java's null is a type-less placeholder assignable to any reference type.
Calling a method on null throws NullPointerException at runtime with no
compile-time protection (unless you use @NonNull annotations). Python's
None at least gives you a clear AttributeError with a precise traceback.
SQL NULL uses three-value logic — a comparison with NULL yields NULL
(not True or False). Writing WHERE col = NULL (wrong) vs
WHERE col IS NULL (correct) is a common mistake. Python's is None
check is unambiguous.
Type-safe languages solve the null problem with algebraic data types:
- Haskell uses
Maybe a— eitherJust a(has a value) orNothing. - Rust uses
Option<T>— eitherSome(T)orNone.
The compiler forces you to handle both cases. Python's type hints + mypy bring you close:
from typing import Optional
def find(name: str) -> Optional[str]: # caller knows None is possible
...# None IS:
print(None is None) # True — singleton
print(type(None).__name__) # NoneType
# None is NOT any other falsy value:
print(None is False) # False
print(None is 0) # False
print(None is "") # False
print(None is []) # False
# All of the following are falsy but NOT None:
for val in [False, 0, 0.0, "", b"", [], {}, set(), ()]:
print(f"{val!r:10} is None: {val is None}")if x is None:
# do something
if x is not None:
# do something elseif x == None: # works but is less idiomatic and can be overridden
...
if x != None: # avoid — use 'is not None'
...Why is?
None is a singleton, so identity (is) is the natural test for it.
Equality (==) can be overridden by custom classes, leading to subtle bugs:
class Tricky:
def __eq__(self, other):
return True # claims to equal everything, including None
t = Tricky()
print(t == None) # True — misleading!
print(t is None) # False — correctThis is a classic Python gotcha:
def add_item(item, target_list=[]):
target_list.append(item)
return target_list
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] — not a fresh list!The default target_list=[] is evaluated once when the function is defined,
so the same list object is reused across all calls that use the default.
Fix: Use None as the default and create a new mutable object inside:
def add_item(item, target_list=None):
if target_list is None:
target_list = []
target_list.append(item)
return target_list
print(add_item(1)) # [1]
print(add_item(2)) # [2] — fresh list each timeThe same pattern applies to dict, set, and any other mutable type.
None is falsy, but so are 0, False, "", [], {}, etc. Using
if not x: to check for None can catch unintended values:
def process_data(data):
if not data: # Danger: treats 0, "", [] as "no data"
print("No data")
else:
print(f"Processing {data}")
process_data(None) # No data (intended)
process_data(0) # No data (but 0 might be a valid count!)
process_data("") # No data (might be a valid empty string)Fix: Be explicit when you mean None:
def process_data(data):
if data is None:
print("No data provided")
elif not data:
print("Data is empty/falsy but not None")
else:
print(f"Processing {data}")Returning None to signal an error can be ambiguous because None is also a
valid "no result" for many functions.
def find_user(username):
# returns a User object or None if not found — ambiguous on error vs not found
...For truly exceptional conditions, prefer raising an exception. When None is
appropriate (e.g., "record not found"), document it clearly and use type hints.
A common source of AttributeError:
user = get_user(id)
city = user.address.city # AttributeError if user or address is NoneFix: Guard at each level:
city = None
if user is not None and user.address is not None:
city = user.address.cityOr use getattr with a default (see Section 9.5).
- Explicit checks — Always test with
is Noneoris not None. - Avoid
if not xwhenNoneis the only sentinel — unless you are sure that0,False, and empty containers are also invalid. - Provide defaults — Use
dict.get(), orx if x is not None else defaultfor a safe fallback. - Document functions that can return
None— especially whenNonehas a special meaning such as "not found". - Prefer
Optional[T]type hints — signal to callers thatNoneis possible, enabling static analysis.
Many built-ins return None to signal an in-place mutation (e.g.,
list.sort(), list.append()). For your own functions, be intentional:
def try_divide(a, b):
if b == 0:
return None # signals failure
return a / b
result = try_divide(10, 0)
if result is None:
print("Cannot divide by zero")
else:
print(result)A function that falls off the end without a return statement implicitly
returns None. This is a frequent source of bugs:
def update_score(score):
if score > 0:
return score + 10
# Forgot the else — returns None for score <= 0!
total = update_score(-5)
print(total + 1) # TypeError: unsupported operand type(s): 'NoneType' and 'int'Always ensure every code path that should return a value does so.
class LazyConnection:
def __init__(self):
self._connection = None
def connect(self):
if self._connection is None:
self._connection = self._create_connection()
return self._connectionNone can be stored as a regular element. Filter it out with a comprehension:
data = [1, None, 3, None, 5]
filtered = [x for x in data if x is not None] # [1, 3, 5]
sum_val = sum(x for x in data if x is not None) # 9Counting None values:
none_count = sum(1 for x in data if x is None) # 2Use None as an explicit "not set" value, and get() to handle missing keys:
config = {"host": "localhost", "port": None}
# Key exists but value is None — get() returns None, not the default!
port = config.get("port", 8080) # None (key exists)
safe_port = config.get("port") or 8080 # 8080, but also collapses 0 -> 8080
# Best: explicitly check for None
port = config["port"] if config["port"] is not None else 8080Removing all None values from a dict:
clean = {k: v for k, v in config.items() if v is not None}Since Python 3.5, Optional[T] from the typing module indicates a value
that can be either T or None.
from typing import Optional
def greet(name: Optional[str] = None) -> str:
if name is None:
return "Hello, stranger!"
return f"Hello, {name}!"Optional[T] is equivalent to Union[T, None]. In Python 3.10+ you can write
T | None directly:
def greet(name: str | None = None) -> str:
...Using type hints lets mypy, pyright, and IDE inspectors warn you when you
forget to check for None before using a value.
After an is None or is not None check, type checkers narrow the type:
def double(value: int | None) -> int:
if value is None:
return 0
return value * 2 # type checker knows value is int hereYou can also use assert for narrowing in code that should never receive
None (this acts as a runtime safety net too):
def process(value: int | None) -> int:
assert value is not None, "value must not be None"
return value * 2Python does not have a built-in ?. ("safe navigation") operator like
JavaScript or C#. However, there are several idiomatic ways to handle None
gracefully.
or returns the first truthy operand, or the last if all are falsy.
value = maybe_none or defaultProblem: If maybe_none is falsy but not None (e.g., 0, False,
[]), you'll get default unexpectedly.
value = maybe_none if maybe_none is not None else defaultThis is the most explicit and recommended approach.
# Safe attribute access using and
result = obj is not None and obj.is_validPrefer is not None over a bare truthiness test when the object could be
any falsy non-None value.
For deeply nested structures, a small helper avoids chains of None guards:
def safe_get(obj, *keys, default=None):
"""Safely traverse nested dicts/lists, returning default if any step fails."""
for key in keys:
try:
obj = obj[key]
except (KeyError, IndexError, TypeError):
return default
return obj
data = {"a": {"b": [1, 2, 3]}}
print(safe_get(data, "a", "b", 1)) # 2
print(safe_get(data, "a", "c", "d")) # NoneSometimes None itself is a legitimate value. Create a unique sentinel to
represent "argument not provided":
_MISSING = object() # unique token — nothing else is identical to it
def process(value=_MISSING):
if value is _MISSING:
value = compute_default()
# value could be None, 0, False, "" — all are valid here
process() # uses computed default
process(None) # explicitly passes NoneThis pattern is used internally by dict.pop(key, default) and many popular
libraries (e.g., dataclasses.MISSING, attrs.NOTHING).
None is a safe default for optional fields because it is immutable:
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class Config:
host: str = "localhost"
port: Optional[int] = None # explicitly optional
# For a mutable optional field, use field(default=None)
tags: Optional[list] = field(default=None)
def __post_init__(self):
if self.tags is None:
self.tags = [] # convert None to empty list after constructionWhen working with JSON APIs or SQL databases, None maps to null / NULL.
import json
data = json.loads('{"name": "Alice", "age": null}')
age = data.get("age")
if age is None:
print("Age missing or null")In SQL (using sqlite3), Python None maps to SQL NULL:
cursor.execute(
"INSERT INTO users (name, age) VALUES (?, ?)",
("Bob", None) # None -> NULL in the database
)
# Reading back: SQL NULL becomes Python None
row = cursor.fetchone()
if row["age"] is None:
print("Age not recorded")Structural pattern matching provides a clean way to dispatch on None:
def describe(value):
match value:
case None:
return "nothing"
case int(n) if n < 0:
return f"negative integer: {n}"
case int(n):
return f"positive integer: {n}"
case str(s):
return f"string: {s!r}"
case _:
return f"something else: {value!r}"
print(describe(None)) # nothing
print(describe(-3)) # negative integer: -3
print(describe("hi")) # string: 'hi'Pattern matching is especially useful when dealing with optional fields in dataclasses or TypedDicts:
from dataclasses import dataclass
@dataclass
class Response:
status: int
body: str | None
def handle(resp: Response):
match resp:
case Response(status=200, body=None):
print("OK, empty body")
case Response(status=200, body=str(text)):
print(f"OK: {text}")
case Response(status=404):
print("Not found")
case _:
print(f"Unexpected: {resp}")getattr with a default is a clean way to safely access attributes that may
not exist or may be None:
class User:
def __init__(self, name, email=None):
self.name = name
self.email = email
user = User("Alice")
# getattr with default avoids AttributeError for missing attributes
email = getattr(user, "email", None)
if email is None:
print("No email on file")
# hasattr checks existence, getattr retrieves with fallback
if hasattr(user, "phone") and user.phone is not None:
print(f"Phone: {user.phone}")Python 3.8+ introduced the walrus operator (:=), which is handy for
None-conditional assignment in a single expression:
import re
text = "Order #12345 received"
if m := re.search(r"#(\d+)", text):
print(f"Order ID: {m.group(1)}") # m is not None here
else:
print("No order ID found")
# In a while loop — process items until None is returned
def next_item():
... # returns an item or None when exhausted
while (item := next_item()) is not None:
process(item)Properties are a natural place to enforce that None is or is not allowed:
class Circle:
def __init__(self, radius: float | None = None):
self.radius = radius # goes through the setter
@property
def radius(self) -> float | None:
return self._radius
@radius.setter
def radius(self, value: float | None):
if value is not None and value < 0:
raise ValueError("Radius must be non-negative")
self._radius = value
@property
def area(self) -> float | None:
if self._radius is None:
return None
import math
return math.pi * self._radius ** 2
c = Circle()
print(c.area) # None — no radius set
c.radius = 5.0
print(c.area) # 78.53981633974483Generators can yield None as a valid value, so callers should use
is not None rather than truthiness to detect end-of-data:
def read_records(filename):
"""Yields records or None for blank lines."""
with open(filename) as f:
for line in f:
stripped = line.strip()
yield stripped if stripped else None
# Collect only real records
records = [r for r in read_records("data.txt") if r is not None]A sentinel-based generator termination pattern:
def generate_chunks(data, size):
for i in range(0, len(data), size):
yield data[i:i + size]
yield None # explicit termination signal
for chunk in generate_chunks(range(10), 3):
if chunk is None:
break
print(list(chunk))In data science, None, numpy.nan, and pandas.NA serve similar but
distinct roles:
import numpy as np
import pandas as pd
# numpy arrays use nan, not None, for missing floats
arr = np.array([1.0, np.nan, 3.0])
print(np.isnan(arr)) # [False True False]
print(arr[1] is None) # False — nan is a float, not None
print(arr[1] == np.nan) # False — nan != nan by IEEE 754
# pandas uses pd.NA (or NaN) and offers isnull()/isna()
s = pd.Series([1, None, 3])
print(s.isnull()) # [False, True, False]
print(pd.isna(None)) # True
print(pd.isna(np.nan)) # True
print(pd.isna(pd.NA)) # True
# None in an object-dtype column vs. NaN in a float column
s_obj = pd.Series(["a", None, "c"])
s_flt = pd.Series([1.0, None, 3.0]) # None becomes NaN here
print(s_obj.dtype) # object
print(s_flt.dtype) # float64
print(s_flt[1]) # nan, not NoneWhen checking for missing data in pandas, always use pd.isna() or
pd.isnull() rather than is None, because NaN and pd.NA will slip
through an is None check.
Inspired by Haskell's Maybe, you can build a thin wrapper that short-circuits
None propagation without nested if guards:
from __future__ import annotations
from typing import Callable, Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
class Maybe(Generic[T]):
"""Wraps a value that may be None, allowing chained transformations."""
def __init__(self, value: T | None):
self._value = value
@classmethod
def of(cls, value: T | None) -> Maybe[T]:
return cls(value)
def bind(self, func: Callable[[T], U | None]) -> Maybe[U]:
if self._value is None:
return Maybe(None)
return Maybe(func(self._value))
def get_or(self, default: T) -> T:
return self._value if self._value is not None else default
def __repr__(self):
return f"Maybe({self._value!r})"
# Usage example
data = {"user": {"address": {"city": "Berlin"}}}
city = (
Maybe.of(data.get("user"))
.bind(lambda u: u.get("address"))
.bind(lambda a: a.get("city"))
.get_or("Unknown")
)
print(city) # Berlin
# With a missing key:
city2 = (
Maybe.of(data.get("user"))
.bind(lambda u: u.get("billing")) # key missing -> None
.bind(lambda b: b.get("city"))
.get_or("Unknown")
)
print(city2) # UnknownNoneis a deliberate, explicit singleton — not a dangerous null pointer. Always useisoris notfor comparison.- Its history matters:
Nonebecame a true keyword in Python 3, making accidental reassignment aSyntaxError. Noneis notFalse,0,"", or[]— all are falsy, but onlyNoneisNone. Useis Nonenotif not x.- Avoid mutable default arguments — use
Noneand create objects inside the function body. - Use
Optional[T]/T | Nonein type hints — let tools catch missing None-checks before runtime. oris not a None coalescer —x or defaultcollapses all falsy values; usex if x is not None else defaultwhen precision matters.- For "argument not provided" when
Noneis a valid argument, use a private_MISSING = object()sentinel. - Newer Python features help: pattern matching (
match/case) and the walrus operator (:=) make None-handling code cleaner. - In data science: distinguish
None,numpy.nan, andpandas.NA; usepd.isna()rather thanis Nonefor missing-data checks.