Skip to main content

Naming Conventions - Writing Code That Reads Like English

Reading time: ~20 minutes | Level: Foundation → Engineering

# A real function from a production codebase (names changed to protect the guilty):
def proc(d, f, s, t=True):
r = []
for i in d:
if i[f] > s:
if t:
r.append(i)
else:
r.append(i[f])
return r

This function has a 100% docstring coverage score. There are no style violations. flake8 is silent. And it is completely unreadable.

The problem is not formatting. The problem is names. d, f, s, t, r, i - six single-letter names for six different things. The reader cannot know what any of them mean without running the code or reading the caller.

Names are the primary documentation in code. They communicate intent, domain meaning, and constraints. A well-named function is self-documenting. A poorly-named function is an obstacle.

What You Will Learn

  • Why naming is the hardest part of programming - the cognitive science behind it
  • Python's complete naming convention system: snake_case, PascalCase, UPPER_SNAKE_CASE, _private, __dunder__
  • How to name variables, functions, classes, modules, and constants at engineering depth
  • The boolean naming pattern: is_, has_, can_, should_
  • Anti-patterns to eliminate: data, info, result, temp, obj, manager
  • Loop variable naming: when single letters are OK and when they are not
  • The length vs clarity tradeoff - context determines the right scope
  • How to rename safely without breaking the codebase

Prerequisites

  • Basic Python syntax: functions, classes, loops, modules
  • Some experience with code review feedback about naming
  • Familiarity with the concept of type annotations (helpful but not required)

Why Naming Is Hard

Phil Karlton, a developer at Netscape, is credited with one of the most quoted observations in software engineering:

"There are only two hard things in Computer Science: cache invalidation and naming things."

The joke surfaces frequently because it resonates with real experience. Naming is hard for a specific reason: a name must capture the semantics of something - what it is, what it does, what constraints it has - in a handful of characters. You are compressing a concept into a label, and that compression is lossy.

The cognitive science is clear. Experiments on program comprehension (Scalabrino et al., 2019) show that:

  1. Readers use variable names to build a mental model of what the code does before reading the logic
  2. If the name is wrong or misleading, readers must spend extra time correcting that mental model
  3. Single-letter and abbreviated names force readers to infer meaning from context - a process that consumes working memory and slows comprehension

Every time you write d instead of document, r instead of results, or proc instead of process_payment, you are forcing the next reader to spend mental effort on decoding that should be spent on understanding the logic.

Python's Naming Convention System

Python has a well-defined set of naming conventions. Unlike in some languages, these conventions are not just style - some of them have semantic meaning enforced by the language or the runtime.

snake_case - Variables, Functions, Methods, Modules

Everything that is not a class name or constant uses snake_case: all lowercase letters, words separated by underscores.

# Variables
user_id = 42
invoice_total = 199.99
is_authenticated = True
retry_count = 0

# Functions
def calculate_monthly_payment(principal, rate, months):
...

def send_password_reset_email(user_email):
...

# Methods
class Order:
def calculate_tax(self):
...

def apply_discount(self, code):
...

# Module names (also lowercase, underscores avoided if possible)
# payment_gateway.py ← good
# PaymentGateway.py ← wrong
# paymentgateway.py ← acceptable, but underscores help readability

PascalCase - Classes (and Type Aliases)

Class names use PascalCase (also called UpperCamelCase): each word starts with a capital letter, no underscores.

# CORRECT
class UserAccount:
...

class InvoiceLineItem:
...

class DatabaseConnectionPool:
...

# Type aliases (PEP 613 style)
from typing import TypeAlias
UserId: TypeAlias = int
EmailAddress: TypeAlias = str

# WRONG
class user_account: # snake_case - looks like a function
...

class userAccount: # camelCase - Java style, not Python
...

UPPER_SNAKE_CASE - Constants

Module-level constants use all caps with underscores. This signals: "this value should not change."

# CORRECT
MAX_RETRY_ATTEMPTS = 3
DEFAULT_TIMEOUT_SECONDS = 30
API_BASE_URL = "https://api.example.com/v2"
SUPPORTED_CURRENCIES = ("USD", "EUR", "GBP", "JPY")

# WRONG
max_retry_attempts = 3 # looks like a regular variable
MaxRetryAttempts = 3 # looks like a class

Important caveat: Python does not enforce immutability for UPPER_SNAKE_CASE names. The convention is a signal to other developers: "don't reassign this." For true immutability, use final from typing (Python 3.8+):

from typing import Final

MAX_RETRY_ATTEMPTS: Final = 3

_single_underscore - Internal / Private Convention

A single leading underscore signals "this is an internal implementation detail." It is a convention, not enforced by the language. IDEs and tools respect it (e.g., from module import * does not import names with leading underscores).

class PaymentProcessor:
def process(self, amount: float) -> bool:
"""Public method - part of the external API."""
validated = self._validate_amount(amount)
if not validated:
return False
return self._submit_to_gateway(amount)

def _validate_amount(self, amount: float) -> bool:
"""Internal validation - not part of the public API."""
return amount > 0 and amount < 1_000_000

def _submit_to_gateway(self, amount: float) -> bool:
"""Internal - specific to this implementation."""
...

# Module-level internals
_connection_cache: dict = {}
_initialized = False

__double_underscore - Name Mangling

A double leading underscore (__name) triggers Python's name mangling: the name is rewritten to _ClassName__name. This is used to prevent accidental overriding in subclasses. It is not privacy enforcement.

class Base:
def __init__(self):
self.__secret = "internal" # stored as _Base__secret

class Child(Base):
def __init__(self):
super().__init__()
self.__secret = "child" # stored as _Child__secret - different!

b = Base()
print(b._Base__secret) # "internal" - accessible, just renamed

Use __ sparingly. Most engineers overuse it thinking it provides privacy. A single underscore communicates "internal" adequately for almost all real cases.

__dunder__ - Magic Methods and Attributes

Names with double underscores on both sides (__init__, __str__, __len__) are reserved for Python's internal protocol. Never create your own __name__ attributes - they belong to the language.

class Temperature:
def __init__(self, celsius: float) -> None:
self.celsius = celsius

def __repr__(self) -> str:
return f"Temperature({self.celsius}°C)"

def __eq__(self, other: object) -> bool:
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius

def __lt__(self, other: "Temperature") -> bool:
return self.celsius < other.celsius

Naming Variables: Be Descriptive, Use Domain Language

The single most important rule for variable names: use the language of the problem domain.

If you are writing code for a banking application, your variable names should sound like banking: account_balance, transaction_id, overdraft_limit, routing_number. Not x, val, data, temp.

# BAD - abstract names with no domain meaning
d = get_data()
for i in d:
r = i['v'] * i['q']
tmp = r * (1 - i['d'])
results.append(tmp)

# GOOD - domain language makes the loop readable
line_items = get_invoice_line_items()
for item in line_items:
gross_amount = item['unit_price'] * item['quantity']
net_amount = gross_amount * (1 - item['discount_rate'])
totals.append(net_amount)

The second version reads like a sentence: "for each item in the invoice line items, compute the gross amount as unit price times quantity, then compute the net amount applying the discount rate."

Avoid Common Anti-Pattern Names

These names appear constantly in beginner code and in legacy systems. They communicate nothing:

Bad nameWhy it's badWhat to use instead
dataEverything is datausers, transactions, config_values
infoInformation about what?user_profile, server_metadata
resultResult of what?filtered_records, payment_response
temp or tmpTemporary how?intermediate_total, staging_record
objObject of what kind?current_user, active_subscription
valValue of what?discount_percentage, retry_delay_ms
d, x, nMeaninglessAnything descriptive
foo, barOnly in examplesAnything descriptive
stuff, thingsPlural of nothingcached_responses, pending_invoices

Abbreviations: The Rule

Use abbreviations only when they are standard in the domain and widely understood:

# OK - universally understood abbreviations
http_status = response.status_code # "http" is standard
user_id = get_user_id() # "id" is universal
max_retries = 3 # "max" is clear
db_connection = connect() # "db" is standard
url = build_endpoint_url() # "url" is universal
api_key = os.environ["API_KEY"] # "api" is standard

# WRONG - project-specific or unclear abbreviations
usr_mgr = UserManager() # "mgr" unclear; "usr" unnecessary
proc_fn = get_processor_function() # both "proc" and "fn" are vague
p = get_product() # "p" is useless
acct = get_account() # "acct" saves 3 characters, costs clarity

The test: would a new engineer who joined the team today understand this abbreviation without being told? If not, spell it out.

Naming Functions: Verb Phrases That Reveal Intent

Functions do things. Their names should be verb phrases that describe what they do.

# BAD - nouns or vague verbs
def data(user_id):
...

def user(user_id):
...

def process(order):
...

def handle(event):
...

def do_stuff(config):
...

# GOOD - verb phrases with domain meaning
def fetch_user_by_id(user_id: int) -> User:
...

def calculate_order_total(order: Order) -> Decimal:
...

def send_confirmation_email(user: User, order: Order) -> None:
...

def validate_credit_card_number(card_number: str) -> bool:
...

def retry_with_exponential_backoff(fn, max_attempts: int = 3):
...

The get_ vs fetch_ vs load_ Distinction

Many teams distinguish between functions that retrieve data from different sources:

def get_user(user_id: int) -> User | None:
"""Return user from in-memory cache or None if not found."""
return _user_cache.get(user_id)

def fetch_user(user_id: int) -> User:
"""Fetch user from database. Raises UserNotFoundError if missing."""
return db.query(User).filter_by(id=user_id).one()

def load_user_from_csv(file_path: str) -> list[User]:
"""Load users from a CSV file and return as a list."""
...

This is a convention your team should agree on, but the underlying principle is that the function name should communicate:

  1. What it returns
  2. Where it gets the data from
  3. What happens when it fails

Command vs Query Naming

The Command-Query Separation (CQS) principle says functions should either return a value (query) or cause a side effect (command), but not both. The name should reflect which:

# QUERIES - return a value, named as noun phrases or get_ verbs
def current_balance(account: Account) -> Decimal:
...

def is_eligible_for_discount(user: User) -> bool:
...

# COMMANDS - cause side effects, named as imperative verbs
def transfer_funds(from_account: Account, to_account: Account, amount: Decimal) -> None:
...

def archive_old_orders(cutoff_date: date) -> None:
...

def notify_user_of_shipment(user: User, tracking_number: str) -> None:
...

When a function both does something and returns a value (which is sometimes unavoidable), the name should emphasize the command: create_user_and_return_id is clearer than user_id.

Boolean Names: The is_, has_, can_, should_ Pattern

Boolean variables and functions that return booleans should always be named as predicates - phrases that read as a yes/no question.

# BAD - ambiguous, could be anything
active = True
email = False
permission = check_permission()
delete = True

# GOOD - reads as a question, answer is obviously yes/no
is_active = True
has_verified_email = False
has_delete_permission = check_delete_permission()
should_delete_on_exit = True

The four prefix patterns and their semantics:

PrefixSemanticsExample
is_Current state or type checkis_authenticated, is_expired, is_empty
has_Possession or completion checkhas_premium_subscription, has_been_processed
can_Capability or permission checkcan_edit, can_access_admin, can_retry
should_Policy or configuration decisionshould_send_email, should_retry_on_failure
# Functions that return booleans
def is_valid_email(address: str) -> bool:
"""Return True if the address has valid email format."""
return "@" in address and "." in address.split("@")[-1]


def has_active_subscription(user_id: int) -> bool:
"""Return True if the user has a subscription that has not expired."""
subscription = Subscription.get_for_user(user_id)
return subscription is not None and not subscription.is_expired()


def can_access_admin_panel(user: User) -> bool:
"""Return True if the user has admin or superuser role."""
return user.role in ("admin", "superuser")


def should_retry_request(attempt_number: int, error: Exception) -> bool:
"""Return True if the request should be retried based on attempt count and error type."""
return (
attempt_number < MAX_RETRY_ATTEMPTS
and isinstance(error, (ConnectionError, TimeoutError))
)

Notice that should_retry_request reads naturally in a caller:

if should_retry_request(attempt, last_error):
wait_and_retry(attempt)

Naming Classes: Noun Phrases, Avoid Suffix Inflation

Classes represent things - entities, concepts, abstractions. Their names should be noun phrases.

# CORRECT - nouns that represent what the class is
class User:
...

class InvoiceLineItem:
...

class EmailNotification:
...

class ConnectionPool:
...

class PaymentTransaction:
...

Avoid the Manager/Handler/Processor Suffix Trap

The suffixes Manager, Handler, Processor, Helper, Util, Service, and Controller are code smells when overused. They signal that the class does not have a clear purpose - it is a grab-bag of functionality.

# BAD - what does UserManager manage? Everything? Nothing specific?
class UserManager:
def create_user(self):
...
def send_email(self):
...
def calculate_discount(self):
...
def generate_report(self):
...

# BETTER - specific classes with clear responsibilities
class UserRepository:
"""Handles persistence: create, read, update, delete users."""
def create(self, email: str, password: str) -> User: ...
def find_by_email(self, email: str) -> User | None: ...

class UserEmailService:
"""Sends user-related emails."""
def send_welcome_email(self, user: User) -> None: ...
def send_password_reset(self, user: User, token: str) -> None: ...

class DiscountCalculator:
"""Computes discounts based on subscription tier and coupon codes."""
def calculate(self, user: User, cart: Cart) -> Decimal: ...

When you find yourself with a Manager class, it is usually a sign that the class has too many responsibilities and should be split.

Exception: Manager is appropriate in Django (where Model.objects is a manager) and in some well-established framework patterns. Use the framework's terminology inside the framework.

Module Names: Short, Lowercase, Purposeful

Module names should be short, lowercase, and descriptive of what the module contains. Underscores are allowed but should be avoided if the name is readable without them.

# GOOD module names
auth.py
models.py
database.py
email_service.py
payment_gateway.py
config.py
utils.py # acceptable as a catch-all, but specific is better

# BAD module names
AuthModule.py # PascalCase, has "Module" suffix
my_utils.py # "my" adds nothing
helper_functions.py # vague
stuff.py # terrible
misc.py # equally terrible

Package (directory) names follow the same rules. The package name payments is better than payment_modules or PaymentsPackage.

Constants: UPPER_SNAKE_CASE and Why It Matters

Constants deserve their own discussion because their naming convention carries semantic meaning beyond just "this is a number."

# At module level - these should never change
MAX_PASSWORD_LENGTH = 128
MIN_PASSWORD_LENGTH = 8
BCRYPT_ROUNDS = 12
JWT_ALGORITHM = "HS256"
JWT_EXPIRY_HOURS = 24
DEFAULT_PAGE_SIZE = 20
MAX_PAGE_SIZE = 100
SUPPORTED_PAYMENT_METHODS = frozenset({"credit_card", "paypal", "bank_transfer"})

Magic Numbers Are the Enemy

A "magic number" is a numeric literal whose meaning is not obvious from context:

# BAD - what does 86400 mean?
if elapsed > 86400:
expire_session()

# BAD - what does 0.15 represent?
tax = subtotal * 0.15

# GOOD - the name explains the meaning
SECONDS_PER_DAY = 86_400
if elapsed > SECONDS_PER_DAY:
expire_session()

DEFAULT_TAX_RATE = 0.15 # 15%
tax = subtotal * DEFAULT_TAX_RATE

Note the use of 86_400 - Python allows underscores in numeric literals as separators (like commas in English notation). This makes large numbers readable.

Configuration Constants vs Hardcoded Constants

Prefer environment variables for values that differ between environments (dev, staging, production):

import os

# Hardcoded - appropriate for universal truths
MAX_CONCURRENT_CONNECTIONS = 100
HTTP_TIMEOUT_SECONDS = 30

# From environment - appropriate for environment-specific values
DATABASE_URL = os.environ["DATABASE_URL"]
SECRET_KEY = os.environ["SECRET_KEY"]
API_ENDPOINT = os.environ.get("API_ENDPOINT", "https://api.example.com")

Naming in Loops: Beyond i, j, k

Single-letter loop variables are so common that many engineers use them automatically. This is a habit worth breaking.

# BAD - what is i? What is j?
for i in users:
for j in i.orders:
process(i, j)

# GOOD - reads like prose
for user in users:
for order in user.orders:
process_user_order(user, order)

The objection engineers raise: "But i in a numeric loop is universal - everyone knows it means index."

This is partially true. In a tight, simple numeric loop, i is acceptable:

# Acceptable - tight scope, purely numeric, no domain meaning
for i in range(10):
print(i)

# Acceptable - mathematical context where i, j, k are standard
matrix = [[0] * n for i in range(n)]

The rule: single-letter loop variables are acceptable only when:

  1. The loop body is 2-3 lines maximum
  2. The variable has no domain meaning - it is purely positional or mathematical
  3. There is no nested loop at the same level

When in doubt, use a semantic name:

# Instead of i, j, k in nested loops:
for row_index, row in enumerate(matrix):
for col_index, value in enumerate(row):
if value > threshold:
flagged_cells.append((row_index, col_index))

The Length vs Clarity Tradeoff

Naming advice often sounds like: "always use long, descriptive names." This is not quite right. Name length should scale with the scope of the variable.

# In a tiny, local scope - short names are fine
squares = [x * x for x in range(10)]

# In a medium-scope function - moderate names
def process_batch(records: list[dict]) -> list[Result]:
results = []
for rec in records: # "rec" is OK - scope is one function
result = transform(rec)
results.append(result)
return results

# In a long function or module scope - full names
def calculate_compounded_annual_growth_rate(
starting_value: float,
ending_value: float,
number_of_years: int,
) -> float:
"""Return CAGR as a decimal (e.g., 0.12 for 12% annual growth)."""
growth_ratio = ending_value / starting_value
return growth_ratio ** (1 / number_of_years) - 1

The principle: the longer a name is in scope, the more descriptive it should be. A comprehension variable lives for one line. A module-level constant lives for the life of the program. Their naming requirements are different.

How to Rename Safely

Renaming in a codebase is one of the most common sources of bugs introduced by "harmless" refactoring. Here is the safe sequence:

Step 1: Use IDE Rename Refactoring

All major Python IDEs (PyCharm, VS Code with Pylance, etc.) have rename refactoring that understands Python's scoping rules. This is safer than find-and-replace because it distinguishes between user the variable and "user" the string literal.

PyCharm: Shift+F6
VS Code: F2

Step 2: Use git grep to Find All Occurrences

After IDE renaming, verify nothing was missed:

git grep -n "old_name"

git grep is faster than regular grep in git repositories and respects .gitignore.

Step 3: Check String References Separately

IDEs do not rename names inside strings. If your code uses reflection or string-based lookups, search for the string:

git grep -n '"old_name"'
git grep -n "'old_name'"

Step 4: Run Your Test Suite

After renaming, run the full test suite. A passing test suite is the final confirmation that the rename did not break anything.

Step 5: Deprecate, Don't Delete (For Public APIs)

If you are renaming a public function or class (one that callers outside your module use), do not delete the old name immediately:

def get_user(user_id: int) -> User:
"""Fetch user by ID. Raises UserNotFoundError if not found."""
...


# Deprecation shim - keep for one release cycle
def fetch_user(user_id: int) -> User:
"""Deprecated: use get_user() instead. Will be removed in v3.0."""
import warnings
warnings.warn(
"fetch_user() is deprecated; use get_user() instead.",
DeprecationWarning,
stacklevel=2,
)
return get_user(user_id)

Naming Gotchas and Edge Cases

Avoid Shadowing Built-ins

Python has a set of built-in names (list, dict, set, type, id, input, format, filter, map, min, max, sum, open, print). Never use them as variable names:

# BAD - shadows the built-in list type
list = [1, 2, 3]
print(list(range(10))) # TypeError: 'list' object is not callable

# BAD - shadows built-in id()
id = user.id
# ... later: id(some_object) will fail

# GOOD - use descriptive names
user_ids = [1, 2, 3]
user_id = user.user_id # or just user.id if user is already clear

Class Names in Module Names

Do not repeat the module name in the class name if the module is always imported:

# module: payment.py
# BAD - redundant when imported as payment.PaymentProcessor
class PaymentProcessor:
...

# GOOD - used as payment.Processor
class Processor:
...

However, if the class is imported directly (from payment import PaymentProcessor), the full name is appropriate because the module context is lost.

Plural vs Singular

Collections are plural. Individual items are singular. This seems obvious but is frequently violated:

# WRONG
for user in users_list: # redundant "list"
process(user)

for user in user: # singular used for a collection
process(user)

# CORRECT
for user in users:
process(user)

# The pattern: singular name = one item, plural name = collection
order = fetch_order(order_id) # singular - one order
orders = fetch_orders_for_user(user_id) # plural - many orders

Interview Questions

Q1: Why is naming considered one of the hardest problems in programming?

Answer: Naming is hard because it requires compressing semantic meaning - what a thing is, what constraints it has, how it behaves - into a concise label. Names must be accurate (correctly describing the thing), complete (not omitting important aspects), concise (short enough to read quickly), and consistent with domain language. They must remain accurate as code evolves, which is particularly difficult because requirements change and code gets extended in ways the original author did not anticipate. A name chosen correctly for an early version of a function may become misleading as the function gains responsibilities. The difficulty is compounded by the cognitive research showing that misleading names actively slow comprehension and increase bug rates.

Q2: What is Python's naming convention system and what is the semantic meaning of each convention?

Answer: Python uses four primary naming conventions: snake_case for variables, functions, methods, and module names; PascalCase for class names and type aliases; UPPER_SNAKE_CASE for module-level constants; and _single_underscore prefix for internal/private implementation details. The conventions are not arbitrary - UPPER_SNAKE_CASE signals immutability by convention (though Python does not enforce it); _underscore prefix signals that the name is part of the implementation rather than the public API; __double_underscore prefix triggers name mangling to prevent accidental override in subclasses. Understanding the semantic signal of each convention allows engineers to communicate implementation intent through naming alone.

Q3: What is the boolean naming pattern and why does it matter?

Answer: Boolean variables and functions returning booleans should use predicate prefixes: is_ for state or type checks (is_authenticated, is_expired), has_ for possession or completion (has_verified_email, has_been_processed), can_ for capability or permission (can_edit, can_retry), and should_ for policy decisions (should_send_email, should_retry_on_failure). The pattern matters because it makes boolean usage read like English prose. if is_authenticated: reads like a sentence; if auth: does not. The pattern also disambiguates between booleans and other types: user_email could be a string or a boolean, while has_verified_email is unambiguous.

Q4: When are single-letter variable names acceptable?

Answer: Single-letter variable names are acceptable in limited, well-defined contexts: in tight mathematical loops where the variable has no domain meaning and is purely positional (for i in range(n)), in list comprehensions with a very short body ([x * x for x in values]), in lambda expressions where the variable scope is a single expression (sorted(items, key=lambda x: x.name)), and in conventional mathematical contexts where single-letter notation is standard (matrix[i][j]). The rule is that the scope of the name should be short - ideally one line, at most a few lines - and the variable should have no domain meaning that a longer name would communicate. In any other context, the name should be descriptive.

Q5: What is the "Manager/Handler/Processor" anti-pattern and how do you fix it?

Answer: The Manager, Handler, Processor, Helper, and Util suffixes are anti-patterns because they describe what a class does at a meta level rather than what it actually represents. A UserManager might handle user creation, authentication, email sending, and reporting - it has no clear single responsibility. When you see these suffixes, the class usually needs to be decomposed into smaller, more focused classes with specific names: UserRepository for persistence, UserEmailService for email, UserAuthenticator for authentication. The fix is to identify the actual responsibilities of the class and name each extracted class after what it represents, not after the generic role it plays. The exception is framework-established terminology like Django's Model.Manager - within a framework, use the framework's conventions.

Q6: How do you rename a symbol safely in a Python codebase?

Answer: Safe renaming follows a sequence: first, use IDE rename refactoring (F2 in VS Code, Shift+F6 in PyCharm) which understands Python scoping and distinguishes between the variable and string literals with the same spelling. Second, use git grep -n "old_name" to verify no occurrences were missed, and separately search for string references that the IDE would not rename. Third, run the full test suite to confirm no runtime references were broken (dynamic attribute access via getattr, string-based dispatch, etc. can reference names as strings). Finally, for public APIs, do not delete the old name immediately - add a deprecation shim that calls the new name and emits a DeprecationWarning, and remove the shim in the next major release.

Practice Challenges

Beginner - The Rename Gauntlet

Every name in the following function is wrong. Rename each one to be clear and descriptive. Do not change the logic.

def f(l, n, t=True):
r = []
c = 0
for i in l:
if i['s'] >= n:
c += 1
if t:
r.append(i)
return r, c

Context: this function filters a list of student records. Each record has a 'score' field. n is a minimum passing score. t controls whether to return the full records or just the count.

Solution
def filter_passing_students(
student_records: list[dict],
minimum_passing_score: int,
return_full_records: bool = True,
) -> tuple[list[dict], int]:
"""
Filter student records by minimum passing score.

Args:
student_records: List of dicts, each containing at least a 'score' key.
minimum_passing_score: Minimum score required to pass.
return_full_records: If True, return full dicts; otherwise return empty list.

Returns:
A tuple of (passing_records, passing_count).
"""
passing_records = []
passing_count = 0

for student in student_records:
if student['score'] >= minimum_passing_score:
passing_count += 1
if return_full_records:
passing_records.append(student)

return passing_records, passing_count

Changes made:

  • ffilter_passing_students - verb phrase, domain-specific
  • lstudent_records - domain noun, plural (it is a collection)
  • nminimum_passing_score - descriptive with units implicit in context
  • treturn_full_records - boolean with return_ prefix that reads naturally in a conditional
  • rpassing_records - describes what the collection contains
  • cpassing_count - describes what is being counted
  • istudent - singular form of the collection being iterated

Intermediate - Name Audit

Read the following class and write a name audit: for each name (class, methods, attributes, parameters), mark it as "acceptable," "marginal," or "poor" with one sentence explaining why. Then rewrite the class with all names improved.

class Proc:
def __init__(self, data, cfg):
self.data = data
self.cfg = cfg
self.res = []
self.ok = False

def run(self, mode=1):
if mode == 1:
for i in self.data:
if i > self.cfg.get('min', 0):
self.res.append(i)
self.ok = True
elif mode == 2:
self.res = sorted(self.data)
self.ok = True

def get_res(self):
return self.res if self.ok else None
Solution

Name Audit:

NameRatingReason
ProcPoorPascalCase OK but "Proc" is meaningless; what does it process?
dataPoorWhat kind of data? Numbers? Records? Any type?
cfgMarginal"cfg" is a recognizable abbreviation but config is clearer
resPoorShort for "result" - could be "results," "response," "resolution"
okPoorBoolean but no is_/has_ prefix; "ok" is vague
runPoorVerb but completely opaque - run what?
modePoorMagic numbers 1 and 2 with no names - what do they mean?
iPoorLoop variable in a non-trivial loop with domain meaning
get_resPoorDouble abbreviation; should use full words

Rewritten class:

from enum import Enum


class FilterMode(Enum):
ABOVE_MINIMUM = "above_minimum"
SORTED = "sorted"


class NumberFilter:
"""Filters or sorts a list of numeric values based on a configurable mode."""

def __init__(self, values: list[float], config: dict) -> None:
self.values = values
self.config = config
self.filtered_results: list[float] = []
self.has_been_processed = False

def apply(self, mode: FilterMode = FilterMode.ABOVE_MINIMUM) -> None:
"""Apply the filter according to the specified mode."""
if mode == FilterMode.ABOVE_MINIMUM:
minimum_value = self.config.get("minimum_value", 0)
self.filtered_results = [
value for value in self.values if value > minimum_value
]
self.has_been_processed = True

elif mode == FilterMode.SORTED:
self.filtered_results = sorted(self.values)
self.has_been_processed = True

def get_results(self) -> list[float] | None:
"""Return filtered results, or None if apply() has not been called."""
return self.filtered_results if self.has_been_processed else None

Advanced - Build a Naming Linter

Write a script that reads a Python file and reports every name that matches the common anti-pattern names: data, info, result, temp, tmp, obj, val, x, y, z, n, d, r, s (as standalone variable names, not as parts of longer names). Report the line number, the context (assignment or function parameter), and the offending name.

Solution
"""
naming_linter.py - find common anti-pattern variable names in Python files.

Usage:
python naming_linter.py src/mymodule.py
"""

import ast
import sys
from pathlib import Path
from dataclasses import dataclass


SUSPICIOUS_NAMES = frozenset({
# Vague generic names
"data", "info", "result", "results", "temp", "tmp",
"obj", "val", "value", "output", "ret",
# Single letters (excluding conventionally acceptable ones)
"d", "r", "s", "n", "k", "v", "p", "q", "t", "m",
# Common placeholder names
"foo", "bar", "baz", "stuff", "things", "misc",
})

# Single-letter names that are conventionally acceptable
ACCEPTABLE_SINGLE_LETTERS = frozenset({"i", "j", "x", "y", "z", "e", "f"})


@dataclass
class Violation:
line: int
col: int
name: str
context: str # "variable", "parameter", "loop_variable"


def find_suspicious_names(tree: ast.AST) -> list[Violation]:
"""Walk the AST and return all suspicious name assignments."""
violations = []

for node in ast.walk(tree):
# Variable assignments: x = ...
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name):
if target.id in SUSPICIOUS_NAMES:
violations.append(Violation(
line=target.lineno,
col=target.col_offset,
name=target.id,
context="variable",
))

# Function parameters
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
for arg in node.args.args + node.args.posonlyargs + node.args.kwonlyargs:
if arg.arg in SUSPICIOUS_NAMES and arg.arg != "self":
violations.append(Violation(
line=arg.lineno,
col=arg.col_offset,
name=arg.arg,
context="parameter",
))

# For loop variables
elif isinstance(node, ast.For):
if isinstance(node.target, ast.Name):
if node.target.id in SUSPICIOUS_NAMES:
violations.append(Violation(
line=node.target.lineno,
col=node.target.col_offset,
name=node.target.id,
context="loop_variable",
))

return sorted(violations, key=lambda v: (v.line, v.col))


def report_violations(file_path: str, violations: list[Violation]) -> None:
"""Print a formatted report of naming violations."""
if not violations:
print(f"{file_path}: no naming violations found.")
return

print(f"\n{file_path} - {len(violations)} suspicious name(s) found:\n")
print(f" {'Line':>5} {'Context':<15} {'Name'}")
print(f" {'-'*5} {'-'*15} {'-'*20}")
for v in violations:
print(f" {v.line:>5} {v.context:<15} '{v.name}'")
print()


def analyze_file(path: Path) -> list[Violation]:
"""Parse and analyze a single Python file."""
try:
source = path.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(path))
return find_suspicious_names(tree)
except (SyntaxError, OSError) as exc:
print(f"Error reading {path}: {exc}")
return []


def main(file_paths: list[str]) -> None:
total_violations = 0
for file_path in file_paths:
path = Path(file_path)
if not path.exists():
print(f"File not found: {file_path}")
continue
violations = analyze_file(path)
report_violations(file_path, violations)
total_violations += len(violations)

print(f"Total: {total_violations} suspicious name(s) across {len(file_paths)} file(s)")


if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python naming_linter.py <file1.py> [file2.py ...]")
sys.exit(1)
main(sys.argv[1:])

Example output:

mymodule.py - 4 suspicious name(s) found:

Line Context Name
----- --------------- --------------------
3 parameter 'data'
7 variable 'result'
12 loop_variable 'd'
18 parameter 'tmp'

Total: 4 suspicious name(s) across 1 file(s)

Quick Reference

CategoryConventionExample
Variablessnake_caseuser_id, retry_count
Functionssnake_case verb phrasecalculate_tax(), send_email()
Methodssnake_case verb phraseself.apply_discount()
ClassesPascalCase noun phraseInvoiceLineItem
ConstantsUPPER_SNAKE_CASEMAX_RETRY_ATTEMPTS
Private_single_underscore prefix_validate_amount()
Name-mangled__double_underscore prefixself.__secret
Booleansis_, has_, can_, should_is_authenticated
CollectionsPlural nounusers, order_ids
Loop variableSingular of collectionfor user in users:
Magic methods__dunder____init__, __str__

Key Takeaways

  • Names are the primary documentation in code. A well-named codebase can be understood without any comments; a poorly-named one cannot be understood despite comments.
  • Python's naming conventions carry semantic meaning: UPPER_SNAKE_CASE signals immutability, _underscore signals internal use, PascalCase signals a class - reading conventions communicates intent instantly.
  • Boolean names should always be predicates with is_, has_, can_, or should_ prefixes so they read naturally in conditionals.
  • Avoid the generic anti-patterns (data, result, info, temp, obj, val) - they describe nothing. Use the domain language of the problem you are solving.
  • The Manager/Handler/Processor suffix trap signals a class with too many responsibilities. Split it into focused classes with specific names.
  • Name length should scale with scope: a one-line comprehension variable can be short; a module-level constant or widely-called function needs full, explicit naming.
  • Rename safely: use IDE refactoring first, then verify with git grep, then run the test suite. For public APIs, use deprecation shims rather than immediate deletion.
© 2026 EngineersOfAI. All rights reserved.