Names are executable architecture. They determine which distinctions survive after implementation details fade from a reader's short-term memory. timeout leaves units and meaning uncertain. connect_timeout_seconds establishes a boundary. data erases a domain object. unbilled_usage tells the next operation what decision has already been made.
Good names cannot repair a confused model, but naming pressure reveals one. If a variable requires a paragraph to distinguish it from three similar values, the code may be missing a type, function, or domain concept. This tutorial treats naming as design work rather than a list of vocabulary rules.
Version note. Name binding and scope are Python language behavior. Examples target Python 3.10 through 3.14 and were verified on CPython 3.14. Fast-local storage, inline caches, and memory representation are CPython implementation details. Naming choices should optimize human reasoning, not interpreter folklore.
Experiment 1: name the meaning, not the container
def overdue_invoice_ids(invoices, today):
result = []
for invoice in invoices:
if invoice["due_on"] < today and not invoice["paid"]:
result.append(invoice["id"])
return result
invoices = [
{"id": "A", "due_on": 10, "paid": False},
{"id": "B", "due_on": 20, "paid": False},
{"id": "C", "due_on": 5, "paid": True},
]
print("[result] overdue invoice ids:", overdue_invoice_ids(invoices, today=15))
Names such as invoice_list, result_list, and item_dict report current representation. Representation is already visible from construction and operations, and it may change. Domain names report why a value exists. overdue_invoice_ids says the output has been filtered, projected to identifiers, and is intended for invoice work.
Representation belongs in a name only when it distinguishes operationally important forms: response_bytes versus decoded text, rows_by_id versus ordered rows, or user_iterator versus a reusable collection. Even then, prefer the consequence over the type spelling. raw_payload may matter because validation has not happened; payload_dict merely repeats implementation.
Avoid generic words such as data, info, object, manager, helper, and process when a domain noun or verb exists. They are not forbidden. A generic serialization function may genuinely accept data, and a resource manager may genuinely manage a lifecycle. The test is whether the name narrows interpretation in its context.
Experiment 2: include units and coordinate systems
from datetime import datetime, timedelta, timezone
def retry_deadline(started_at, retry_window_seconds):
return started_at + timedelta(seconds=retry_window_seconds)
started_at = datetime(2026, 8, 30, 12, 0, tzinfo=timezone.utc)
deadline = retry_deadline(started_at, retry_window_seconds=30)
print("[result] retry deadline UTC:", deadline)
The numeric parameter states its unit while the timestamp uses a type that preserves timezone information. Bare timeout, duration, size, offset, and rate invite incompatible interpretations. Include units at boundaries: timeout_seconds, size_bytes, price_cents, angle_radians, utc_offset_minutes.
Inside a tiny scope where a type carries units, shorter names can be enough. A timedelta named retry_window need not say seconds because it is not a naked number. Better types reduce naming burden and prevent invalid arithmetic. Naming and modeling are substitutes only up to a point; a suffix cannot stop a caller from passing milliseconds.
Coordinate systems deserve the same treatment. created_at_utc and display_time_local prevent accidental comparison. source_offset and destination_index distinguish reference frames. Security code should name whether a path is user-supplied, normalized, or authorized; those stages are not interchangeable strings.
Public keyword names are API. Calling retry_deadline(started_at, retry_window_seconds=30) binds by spelling. Renaming the parameter breaks callers even if position and behavior remain. Treat keyword-capable parameters as compatibility commitments.
Experiment 3: Boolean names should form claims
def may_publish(article):
has_title = bool(article.get("title"))
is_reviewed = article.get("status") == "reviewed"
author_can_publish = "publish" in article.get("permissions", ())
return has_title and is_reviewed and author_can_publish
article = {
"title": "Names",
"status": "reviewed",
"permissions": ["publish"],
}
print("[check] article may publish:", may_publish(article))
has_title, is_reviewed, and author_can_publish read as propositions. Names such as title_check, review_status, or publish_flag force readers to discover whether truth means success, presence, or a request.
Choose prefixes according to semantics, not rigid style: is_ for state or classification, has_ for possession, can_ for capability, should_ for a policy decision, and verbs such as matches for predicates. may_publish signals authorization and current conditions; publish should perform the effect.
Avoid negative Boolean names where possible. if not disable_cache and if not user_is_not_active require mental inversion. Some negative domain states are natural, such as is_deleted, but parameters can often choose a positive default: use_cache=True. Double negatives are a refactoring alarm.
A Boolean may hide insufficient information. save() returning false cannot distinguish validation rejection, conflict, unavailable storage, or an ordinary no-op. Use a result enum, optional value, or exception when callers need different responses. A better variable name cannot recover discarded states.
Experiment 4: scope determines how much a name must carry
def index_by_email(users):
users_by_email = {}
for user in users:
normalized_email = user["email"].strip().casefold()
users_by_email[normalized_email] = user
return users_by_email
users = [
{"name": "Ada", "email": " ADA@example.com "},
{"name": "Grace", "email": "grace@example.com"},
]
print("[result] normalized email index keys:", sorted(index_by_email(users)))
Short names work in small, conventional scopes. user inside a four-line loop is clearer than current_user_record. i can be appropriate for a tiny positional loop, though index is better when passed onward or mixed with other numbers. The farther a name travels, the more context it must carry.
Long functions create pressure for long names because values from different phases coexist. Before inventing original_unvalidated_customer_input_dictionary, split validation and conversion into functions or introduce types whose names establish the phase. Function boundaries let local names become simple again.
Collections benefit from plurality and relationship names. users_by_email tells readers it is a mapping and identifies the key without appending _dict. email_to_user is also precise. Pick the form consistent with nearby code. A mapping named users can be fine if only values matter, but becomes misleading when iteration yields email keys.
Names should survive nearby refactors. users_by_email remains true if the implementation changes from dict to a specialized mapping. user_dict may not.
Experiment 5: avoid shadowing vocabulary readers depend on
import builtins
def summarize(values):
total = builtins.sum(values)
smallest = builtins.min(values, default=None)
return total, smallest
print("[result] total and smallest value:", summarize([4, 2, 7]))
The explicit builtins qualification makes this block robust, but normal code should simply avoid local variables named sum, min, list, str, id, or input when those built-ins may be needed. Shadowing is legal Python name binding, not a syntax error. The later failure often appears as 'int' object is not callable, far from the assignment that replaced the expected meaning.
Also avoid shadowing imported modules and outer variables unintentionally. A parameter named json prevents access to the imported json module in that scope. A nested function assigning status creates a local unless declared nonlocal, which can produce UnboundLocalError when it first tries to read the outer name.
Not every collision is harmful. A method parameter named id may be conventional in a narrow ORM layer that never calls the built-in. Renaming it to identifier_value can make the domain less recognizable. Consider scope and likely operations rather than obeying a universal blacklist.
Python resolves local, enclosing, global, and built-in scopes according to language rules. CPython may store locals in indexed frame slots, but longer names do not make access slower in a meaningful way. Readability, API consistency, and domain precision should decide spelling.
Experiment 6: verbs reveal effects and ownership
class DraftStore:
def __init__(self):
self._drafts = {}
def find(self, draft_id):
return self._drafts.get(draft_id)
def require(self, draft_id):
try:
return self._drafts[draft_id]
except KeyError:
raise LookupError(f"unknown draft: {draft_id}") from None
def save(self, draft_id, content):
self._drafts[draft_id] = content
store = DraftStore()
store.save("intro", "Naming matters")
print("[result] missing draft lookup:", store.find("missing"))
print("[result] required intro draft:", store.require("intro"))
Method verbs establish behavior. find suggests absence is ordinary and returns an optional value. require promises a value or raises. save mutates storage. A vague get could mean retrieval, construction, caching, network I/O, or exception on absence. Familiar collection APIs make get understandable, but domain services often benefit from stronger verbs.
Use parse when interpreting a representation, validate when checking without transformation, normalize when producing a canonical form, load when crossing storage, and create when making a new entity. These conventions are not laws, but inconsistent verbs hide boundaries. A function called validate_user should not silently save the user.
Ownership belongs in names and APIs. borrow_connection suggests the caller must return something; a context manager can encode that obligation more strongly. new_session suggests a distinct object; session might return shared state. Where correctness matters, structure should enforce the promise and naming should advertise it.
Names at public boundaries
Public module names, classes, functions, parameters, exceptions, environment variables, configuration keys, command options, and serialized fields all outlive local refactors. Renaming can break imports, keyword calls, stored documents, dashboards, and operational instructions. Search beyond Python references before changing them.
Aliases and deprecation periods are justified for shipped external APIs or persisted formats, not automatically for private code. Inside one codebase, an atomic rename is usually better than supporting two names indefinitely. The right migration depends on concrete consumers.
Consistency reduces translation cost. If the domain says account, do not alternate among customer, tenant, and client unless they are genuinely different concepts. A glossary can help in a large system, but code remains the authoritative test: if two words behave differently, define the distinction in types and operations.
Avoid encoding types in every name as a substitute for annotations. user_id_string is noisy if the signature says user_id: str. But semantic wrappers are often stronger than both: a UserId distinct from OrderId prevents accidental interchange that suffixes merely warn about.
Rename by following behavior
Before renaming, identify what the value actually represents at each point. A variable named users may begin as request dictionaries, become validated objects, then become IDs after reassignment. The best fix is not one broad name; it is separate bindings such as submitted_users, valid_users, and user_ids, or separate functions for each phase.
Use automated symbol-aware rename tools for identifiers, then inspect strings, dynamic attribute access, templates, configuration, reflection, and documentation. Run tests that exercise keyword arguments and serialization. A text replacement can alter unrelated concepts with the same spelling.
Do not combine a rename with behavioral refactoring unless necessary. A pure rename is easy to verify because runtime behavior should remain identical. Once names accurately expose phases, deeper design changes become safer.
Naming as a diagnostic
Placeholder names during exploration are normal. Before the code becomes a maintained boundary, ask why each important value exists, who owns it, what phase it represents, and which operations are valid. Difficulty answering points to missing design.
Repeated qualifiers can reveal an absent object. Functions named calculate_invoice_total, format_invoice_total, and validate_invoice_total may belong around an InvoiceTotal value with currency and rounding policy. Conversely, creating a class merely to avoid repeating a noun can overcomplicate simple procedural code. Let behavior and invariants, not aesthetics, drive extraction.
Comments should explain decisions that names and structure cannot. eligible_users is better than users # users who are eligible. A useful comment explains why suspended accounts remain eligible during a migration or links to the governing policy.
Exercises: expose the concept
- Find five representation names such as
data,result_list, oritem_dict. Rename only those whose domain meaning is clear. - Audit numeric API parameters for units, time zones, coordinate systems, and currencies. Improve one boundary with a type rather than a suffix.
- Rewrite three Boolean names as readable claims and remove one double negative without changing defaults.
- Split a long function where names carry several phase qualifiers. Compare local names before and after.
- Shadow a built-in intentionally, observe the resulting failure, then choose the smallest clear rename.
- Rename a public keyword parameter in a test fixture and inventory every compatibility surface it affects before deciding a migration.
Keep this model
Names preserve distinctions: domain meaning, units, phase, ownership, effect, and absence semantics. Name values by why they exist, let scopes and types carry context, make Booleans readable as claims, and choose verbs that reveal effects.
Python's binding rules determine what a name refers to; CPython's storage does not make descriptive names expensive. Public names are compatibility surfaces. Local naming friction is design feedback. Use it to discover missing boundaries rather than papering over uncertainty with longer generic phrases.