Python Naming Conventions Practice Problems & Exercises
Practice: Naming Conventions
← Back to lessonEasy
Predict the output. A helper checks whether a name looks like valid Python snake_case — all lowercase letters, digits, and underscores, starting with a letter or underscore.
import re
def is_snake_case(name: str) -> bool:
return bool(re.match(r'^[a-z_][a-z0-9_]*$', name))
print(is_snake_case("user_id"))
print(is_snake_case("retry_count"))
print(is_snake_case("RetryCount"))
print(is_snake_case("retryCount"))Solution
import re
def is_snake_case(name: str) -> bool:
return bool(re.match(r'^[a-z_][a-z0-9_]*$', name))
print(is_snake_case("user_id"))
print(is_snake_case("retry_count"))
print(is_snake_case("RetryCount"))
print(is_snake_case("retryCount"))
Output:
True
True
False
False
How it works: The regex ^[a-z_][a-z0-9_]*$ matches strings that start with a lowercase letter or underscore, followed by any combination of lowercase letters, digits, and underscores. "user_id" and "retry_count" both match. "RetryCount" fails because it contains uppercase letters (PascalCase — valid for class names, not variables). "retryCount" fails on the capital C (camelCase — common in Java/JavaScript, not Python).
Key insight: PEP 8 is Python's official style guide. snake_case is mandated for variables, function names, method names, and module names. Following this convention makes Python code instantly recognizable and consistent across the ecosystem. Linters like flake8 and formatters like black enforce this automatically.
Expected Output
True\nTrue\nFalse\nFalseHints
Hint 1: In Python, variables and functions use snake_case: all lowercase with underscores.
Hint 2: hasattr() checks whether an object has a given attribute name.
Hint 3: Class-style names (PascalCase) and camelCase are valid identifiers but violate PEP 8 for variables.
Predict the output. Four names are checked against a PascalCase validator. Understand which naming patterns Python classes should follow.
import re
def is_pascal_case(name: str) -> bool:
return bool(re.match(r'^[A-Z][a-zA-Z0-9]*$', name))
print(is_pascal_case("UserAccount"))
print(is_pascal_case("InvoiceLineItem"))
print(is_pascal_case("user_account"))
print(is_pascal_case("userAccount"))Solution
import re
def is_pascal_case(name: str) -> bool:
return bool(re.match(r'^[A-Z][a-zA-Z0-9]*$', name))
print(is_pascal_case("UserAccount"))
print(is_pascal_case("InvoiceLineItem"))
print(is_pascal_case("user_account"))
print(is_pascal_case("userAccount"))
Output:
True
True
False
False
How it works: The regex requires the first character to be an uppercase letter, followed by any alphanumeric characters. "UserAccount" and "InvoiceLineItem" are correctly formatted PascalCase class names. "user_account" is snake_case — it looks like a function or variable, not a class. "userAccount" is camelCase — common in Java, wrong in Python.
Key insight: PascalCase for class names is one of the most universally respected Python conventions. When a reader sees UserAccount(), they immediately understand it is a class instantiation. When they see user_account = ..., they understand it is a variable. This visual distinction is part of what makes Python code readable across teams and projects.
Expected Output
True\nTrue\nFalse\nFalseHints
Hint 1: Class names in Python use PascalCase (UpperCamelCase): each word starts with a capital letter.
Hint 2: inspect.isclass() returns True if the object is a class.
Hint 3: A class defined as class user_account violates PEP 8 even if Python accepts it.
Predict the output. Module constants are defined and accessed. One uses typing.Final for stricter enforcement.
from typing import Final
DEFAULT_TIMEOUT_SECONDS = 30
API_BASE_URL = "https://api.example.com/v2"
MAX_RETRY_ATTEMPTS: Final = 3
print(DEFAULT_TIMEOUT_SECONDS)
print(API_BASE_URL)
print(MAX_RETRY_ATTEMPTS)
# Final is a type hint annotation, not a runtime lock
# The value is still accessible at runtime
import typing
print(typing.get_type_hints(
type('C', (), {'x': MAX_RETRY_ATTEMPTS})
) == {})Solution
from typing import Final
DEFAULT_TIMEOUT_SECONDS = 30
API_BASE_URL = "https://api.example.com/v2"
MAX_RETRY_ATTEMPTS: Final = 3
print(DEFAULT_TIMEOUT_SECONDS)
print(API_BASE_URL)
print(MAX_RETRY_ATTEMPTS)
import typing
print(typing.get_type_hints(
type('C', (), {'x': MAX_RETRY_ATTEMPTS})
) == {})
Output:
30
https://api.example.com/v2
3
True
How it works: The first three prints access the constants normally — they behave like any other variable at runtime. DEFAULT_TIMEOUT_SECONDS, API_BASE_URL, and MAX_RETRY_ATTEMPTS all print their assigned values. The fourth print checks that typing.get_type_hints on a dynamically created class with x = 3 (not annotated) returns an empty dict — confirming Final is a type-level annotation, not a runtime attribute lock. Static type checkers (mypy, pyright) will flag attempts to reassign a Final variable, but Python itself will not raise an error.
Key insight: UPPER_SNAKE_CASE is a social contract. It tells every developer who reads the code: "do not reassign this." typing.Final adds machine-checkable enforcement via type checkers. For truly immutable collections, use tuple instead of list: SUPPORTED_CURRENCIES = ("USD", "EUR", "GBP") cannot be modified in place.
Expected Output
30\nhttps://api.example.com/v2\n3\nTrueHints
Hint 1: Module-level constants use UPPER_SNAKE_CASE: all caps with underscores.
Hint 2: Python does not enforce immutability — UPPER_SNAKE_CASE is a signal to other developers.
Hint 3: typing.Final can be used to mark a constant as truly final for type checkers.
Predict the output. Boolean variables are assigned following the naming conventions from the lesson. Trace the logic.
is_authenticated = True
has_verified_email = False
can_edit = True
should_send_welcome_email = False
# Simulate a permission check function
def can_access_dashboard(is_auth: bool, has_email: bool) -> bool:
return is_auth and has_email
print(is_authenticated)
print(has_verified_email)
print(can_edit)
print(can_access_dashboard(is_authenticated, has_verified_email))
print(should_send_welcome_email or can_edit)Solution
is_authenticated = True
has_verified_email = False
can_edit = True
should_send_welcome_email = False
def can_access_dashboard(is_auth: bool, has_email: bool) -> bool:
return is_auth and has_email
print(is_authenticated)
print(has_verified_email)
print(can_edit)
print(can_access_dashboard(is_authenticated, has_verified_email))
print(should_send_welcome_email or can_edit)
Output:
True
False
True
False
True
How it works: is_authenticated is True, has_verified_email is False, can_edit is True, should_send_welcome_email is False. can_access_dashboard(True, False) returns True and False = False. The final expression is False or True = True.
Key insight: The prefix conventions carry semantic meaning beyond naming style. is_ signals a current state check, has_ signals possession or completion, can_ signals a capability or permission, and should_ signals a policy-level decision. These distinctions help readers understand the nature of the condition at a glance. if is_authenticated reads like English; if authenticated forces the reader to infer whether this is a boolean or some other type.
Expected Output
True\nFalse\nTrue\nFalse\nTrueHints
Hint 1: Boolean variables and functions that return booleans should read as yes/no questions.
Hint 2: Prefixes: is_ for state checks, has_ for possession, can_ for capability, should_ for policy.
Hint 3: if is_authenticated reads naturally; if authenticated is ambiguous.
Medium
Predict the output. A class has a public method and a private helper. Explore whether Python actually restricts access to _private members.
class PaymentProcessor:
def process(self, amount: float) -> str:
result = self._validate_amount(amount)
if result:
return "public_method called"
return "invalid amount"
def _validate_amount(self, amount: float) -> bool:
return amount > 0
p = PaymentProcessor()
print(p.process(100.0))
# _private is accessible from outside — it's just a convention
print(p._validate_amount(50.0) and "_internal_helper called")
# Both names appear in __dict__
attrs = [k for k in vars(PaymentProcessor) if not k.startswith('__')]
print('process' in attrs)
print('_validate_amount' in attrs)Solution
class PaymentProcessor:
def process(self, amount: float) -> str:
result = self._validate_amount(amount)
if result:
return "public_method called"
return "invalid amount"
def _validate_amount(self, amount: float) -> bool:
return amount > 0
p = PaymentProcessor()
print(p.process(100.0))
print(p._validate_amount(50.0) and "_internal_helper called")
attrs = [k for k in vars(PaymentProcessor) if not k.startswith('__')]
print('process' in attrs)
print('_validate_amount' in attrs)
Output:
public_method called
_internal_helper called
True
True
How it works: p.process(100.0) calls _validate_amount(100.0) internally, which returns True, so process returns "public_method called". Despite the leading underscore, p._validate_amount(50.0) is perfectly accessible from outside the class — Python does not restrict it. The and short-circuits to the string because True and "string" evaluates to the string. vars(PaymentProcessor) reveals that both process and _validate_amount are in the class __dict__, with no difference in storage.
Key insight: The single underscore is purely a social convention. It tells developers: "this method is an implementation detail — don't call it from outside the class, and don't rely on its signature staying stable." IDEs will typically hide _private members from auto-complete suggestions, and from module import * skips them. But nothing in the Python runtime enforces privacy. The underscore is a message, not a lock.
Expected Output
public_method called\n_internal_helper called\nTrue\nTrueHints
Hint 1: A single leading underscore is a convention, not enforced by Python.
Hint 2: You can still access _private attributes directly — the underscore is a signal to developers.
Hint 3: from module import * skips names with a leading underscore unless __all__ is defined.
Predict the output. Name mangling with double underscores ensures subclass attributes do not collide with parent attributes of the same name.
class Base:
def __init__(self):
self.__secret = "base_secret"
def get_secret(self):
return self.__secret
class Child(Base):
def __init__(self):
super().__init__()
self.__secret = "child_secret"
def get_own_secret(self):
return self.__secret
b = Base()
c = Child()
print(c.get_secret())
print(c.get_own_secret())
# Direct access via mangled name still works
print(c._Base__secret)
print(c._Child__secret == "child_secret")Solution
class Base:
def __init__(self):
self.__secret = "base_secret"
def get_secret(self):
return self.__secret
class Child(Base):
def __init__(self):
super().__init__()
self.__secret = "child_secret"
def get_own_secret(self):
return self.__secret
b = Base()
c = Child()
print(c.get_secret())
print(c.get_own_secret())
print(c._Base__secret)
print(c._Child__secret == "child_secret")
Output:
base_secret
child_secret
base_secret
True
How it works: When Python encounters self.__secret inside Base, it mangles the name to self._Base__secret. When Child.__init__ assigns self.__secret, Python mangles it to self._Child__secret — a completely different attribute. So the Child instance has two separate attributes: _Base__secret = "base_secret" (set by super().__init__()) and _Child__secret = "child_secret" (set by Child.__init__). c.get_secret() calls the method defined in Base, which accesses _Base__secret, returning "base_secret". c.get_own_secret() accesses _Child__secret, returning "child_secret".
Key insight: Name mangling's purpose is to prevent accidental attribute overriding in subclasses — not to hide data. It is useful in frameworks and libraries where you cannot control what subclasses will do. In most application code, a single underscore (_secret) is sufficient. Overusing __ makes code harder to test (you cannot mock __private methods easily) and harder to debug.
Expected Output
base_secret\nchild_secret\nbase_secret\nTrueHints
Hint 1: A double leading underscore triggers name mangling: __secret becomes _ClassName__secret.
Hint 2: Subclasses cannot accidentally override __secret from the parent — each gets its own mangled name.
Hint 3: The mangled name is still accessible externally using the full mangled form.
Predict the output. A Temperature class implements three dunder methods. Trace each operation.
class Temperature:
def __init__(self, celsius: float) -> None:
self.celsius = celsius
def __repr__(self) -> str:
return f"Temperature({self.celsius}\u00b0C)"
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
boiling = Temperature(100)
freezing = Temperature(0)
also_boiling = Temperature(100)
print(repr(boiling))
print(boiling == also_boiling)
print(boiling == freezing)
print(freezing < boiling)Solution
class Temperature:
def __init__(self, celsius: float) -> None:
self.celsius = celsius
def __repr__(self) -> str:
return f"Temperature({self.celsius}\u00b0C)"
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
boiling = Temperature(100)
freezing = Temperature(0)
also_boiling = Temperature(100)
print(repr(boiling))
print(boiling == also_boiling)
print(boiling == freezing)
print(freezing < boiling)
Output:
Temperature(100°C)
True
False
True
How it works: repr(boiling) calls __repr__, which uses an f-string with the Unicode degree symbol \u00b0 to produce "Temperature(100°C)". boiling == also_boiling calls __eq__: both have celsius = 100, so it returns True. boiling == freezing compares 100 == 0, returning False. freezing < boiling calls __lt__: 0 < 100 is True.
Key insight: Dunder methods integrate your class with Python's built-in protocols. Implementing __eq__ enables ==, __lt__ enables < (and with functools.total_ordering, all comparison operators), and __repr__ makes debugging far easier. The NotImplemented sentinel in __eq__ tells Python to try the reverse operation on the other object rather than raising TypeError — a subtle but important detail for interoperability. Never invent your own __name__ attributes — they belong to the language.
Expected Output
Temperature(100°C)\nTrue\nFalse\nTrueHints
Hint 1: __repr__ should return a string that could recreate the object or clearly identifies it.
Hint 2: __eq__ is called when using == between two objects.
Hint 3: __lt__ enables < comparisons and is used by sorted() and min()/max().
Predict the output. Two functions compute the same result. The first uses single-letter names; the second uses domain names. Verify they are equivalent.
# Original — unreadable
def proc(d, f, s):
r = []
for i in d:
v = i[f] * i[s]
r.append(v)
return sum(r)
# Refactored — readable
def calculate_order_subtotal(line_items, price_field, qty_field):
totals = []
for item in line_items:
line_total = item[price_field] * item[qty_field]
totals.append(line_total)
return sum(totals)
items = [
{"unit_price": 45.0, "quantity": 3},
{"unit_price": 22.5, "quantity": 6},
]
result_original = proc(items, "unit_price", "quantity")
result_refactored = calculate_order_subtotal(items, "unit_price", "quantity")
print(result_original)
print(result_refactored * 0.9)
print(result_original == result_refactored)Solution
def proc(d, f, s):
r = []
for i in d:
v = i[f] * i[s]
r.append(v)
return sum(r)
def calculate_order_subtotal(line_items, price_field, qty_field):
totals = []
for item in line_items:
line_total = item[price_field] * item[qty_field]
totals.append(line_total)
return sum(totals)
items = [
{"unit_price": 45.0, "quantity": 3},
{"unit_price": 22.5, "quantity": 6},
]
result_original = proc(items, "unit_price", "quantity")
result_refactored = calculate_order_subtotal(items, "unit_price", "quantity")
print(result_original)
print(result_refactored * 0.9)
print(result_original == result_refactored)
Output:
270.0
243.0
True
How it works: For items[0]: 45.0 * 3 = 135.0. For items[1]: 22.5 * 6 = 135.0. Total: 135.0 + 135.0 = 270.0. Both functions produce 270.0. The second print is 270.0 * 0.9 = 243.0 (a 10% discount applied). The final print confirms identity: 270.0 == 270.0 is True.
Key insight: The two functions are computationally identical — the refactoring changes nothing about what the code does. But the second version communicates intent. A new engineer reading calculate_order_subtotal immediately understands: this computes a monetary total from a list of line items. Reading proc provides no such information. The naming cost is near zero (a few extra characters); the comprehension benefit is enormous. This is why the lesson calls names "the primary documentation."
Expected Output
270.0\n243.0\nTrueHints
Hint 1: Both functions do identical computation — the difference is only naming.
Hint 2: The refactored version uses domain language: unit_price, quantity, discount_rate.
Hint 3: Both should produce the same numeric result — trace the arithmetic.
Hard
Predict the output. Explore how name mangling affects the attribute dictionary and external access patterns.
class BankAccount:
def __init__(self, owner: str, balance: float):
self.__owner = owner
self.__balance = balance
def deposit(self, amount: float) -> None:
self.__balance += amount
def get_balance(self) -> float:
return self.__balance
account = BankAccount("Alice", 1000.0)
account.deposit(500.0)
# How many instance attributes does the object have?
instance_attrs = vars(account)
print(len(instance_attrs))
# What are the actual stored names?
for key in sorted(instance_attrs.keys()):
print(key)
# Can we access via mangled name from outside?
print(account._BankAccount__balance == 1500.0)
# What about direct __balance access outside the class?
try:
_ = account.__balance
except AttributeError:
print("AttributeError")Solution
class BankAccount:
def __init__(self, owner: str, balance: float):
self.__owner = owner
self.__balance = balance
def deposit(self, amount: float) -> None:
self.__balance += amount
def get_balance(self) -> float:
return self.__balance
account = BankAccount("Alice", 1000.0)
account.deposit(500.0)
instance_attrs = vars(account)
print(len(instance_attrs))
for key in sorted(instance_attrs.keys()):
print(key)
print(account._BankAccount__balance == 1500.0)
try:
_ = account.__balance
except AttributeError:
print("AttributeError")
Output:
2
_BankAccount__balance
_BankAccount__owner
True
AttributeError
How it works: When Python compiles the __init__ method of BankAccount, it rewrites self.__owner to self._BankAccount__owner and self.__balance to self._BankAccount__balance. The instance dictionary therefore contains two attributes: _BankAccount__balance and _BankAccount__owner (sorted alphabetically). len(instance_attrs) is 2. After deposit(500.0), _BankAccount__balance is 1500.0, so the comparison is True. Finally, account.__balance raises AttributeError because no attribute named __balance exists — the mangled version is _BankAccount__balance. The mangling happens at compile time within the class body; code outside the class does not get the same transformation applied.
Key insight: Name mangling is a compile-time transformation that happens inside the class body only. It is not dynamic — Python does not mangle names in code that runs outside the class. This asymmetry is why account.__balance fails: the interpreter looks for a literal attribute named __balance, which does not exist. The mangled form _BankAccount__balance is always accessible externally, which is why mangling is anti-subclass-collision protection, not true encapsulation.
Expected Output
2\n_BankAccount__balance\n_BankAccount__owner\nTrue\nAttributeErrorHints
Hint 1: vars(obj) returns the instance __dict__, which contains mangled names, not the original names.
Hint 2: Direct access via obj.__balance raises AttributeError outside the class.
Hint 3: The mangled form obj._BankAccount__balance still works externally.
Predict the output. Three focused classes replace a bloated UserManager. Trace the interactions.
from dataclasses import dataclass
@dataclass
class User:
user_id: str
name: str
email: str
is_active: bool = True
class UserRepository:
def __init__(self):
self._store: dict = {}
def save(self, user: User) -> None:
self._store[user.user_id] = user
def find_by_id(self, user_id: str) -> User | None:
return self._store.get(user_id)
class UserEmailService:
def build_welcome_message(self, user: User) -> str:
return f"Welcome, {user.name}!"
class SubscriptionChecker:
def is_eligible_for_trial(self, user: User) -> bool:
return user.is_active
repo = UserRepository()
mailer = UserEmailService()
checker = SubscriptionChecker()
alice = User(user_id="u1", name="Alice", email="[email protected]")
repo.save(alice)
found = repo.find_by_id("u1")
print(found.email)
print(checker.is_eligible_for_trial(found))
print(mailer.build_welcome_message(found))
print(repo.find_by_id("u1").name)Solution
from dataclasses import dataclass
@dataclass
class User:
user_id: str
name: str
email: str
is_active: bool = True
class UserRepository:
def __init__(self):
self._store: dict = {}
def save(self, user: User) -> None:
self._store[user.user_id] = user
def find_by_id(self, user_id: str) -> User | None:
return self._store.get(user_id)
class UserEmailService:
def build_welcome_message(self, user: User) -> str:
return f"Welcome, {user.name}!"
class SubscriptionChecker:
def is_eligible_for_trial(self, user: User) -> bool:
return user.is_active
repo = UserRepository()
mailer = UserEmailService()
checker = SubscriptionChecker()
repo.save(alice)
found = repo.find_by_id("u1")
print(found.email)
print(checker.is_eligible_for_trial(found))
print(mailer.build_welcome_message(found))
print(repo.find_by_id("u1").name)
Output:
True
Welcome, Alice!
Alice
How it works: alice is a User dataclass with the given field values. repo.save(alice) stores it under key "u1". repo.find_by_id("u1") retrieves it — the email is "[email protected]". checker.is_eligible_for_trial(found) returns found.is_active, which is True (the default). mailer.build_welcome_message(found) builds the greeting from found.name. The final lookup retrieves Alice again and prints her name.
Key insight: The class names encode the domain: UserRepository signals persistence operations, UserEmailService signals email logic, SubscriptionChecker signals eligibility business logic. Each class has one reason to change. Compare this to a hypothetical UserManager that bundles all three responsibilities — changing the email template would require touching the same class that handles database queries. Well-named, focused classes also make testing simpler: you can test UserEmailService without a database, and UserRepository without an email server.
Expected Output
[email protected]\nTrue\nWelcome, Alice!\nAliceHints
Hint 1: Each class should have a focused responsibility that its name clearly communicates.
Hint 2: UserRepository handles persistence; UserEmailService handles emails.
Hint 3: A class named UserManager that does everything violates the Single Responsibility Principle.
Write code that audits a set of names and classifies each as constant, class_name, function_or_variable, or boolean, then verify counts and specific lookups. The classification rules: all-caps with underscores = constant; starts with uppercase letter = class_name; starts with is_, has_, can_, or should_ = boolean; otherwise = function_or_variable.
def classify_name(name: str) -> str:
if name == name.upper() and '_' in name:
return "constant"
if name[0].isupper():
return "class_name"
if any(name.startswith(p) for p in ("is_", "has_", "can_", "should_")):
return "boolean"
return "function_or_variable"
names = [
"MAX_CONNECTIONS",
"OrderService",
"order_service",
"calculate_tax",
"is_expired",
"has_premium",
"DEFAULT_TIMEOUT",
"API_BASE_URL",
"UserRepository",
]
classified = {n: classify_name(n) for n in names}
# Count how many are constants
constants = [n for n, c in classified.items() if c == "constant"]
print(len(constants))
# Is "order_service" classified as a class_name?
print(classified["order_service"] == "class_name")
# Is "is_expired" classified as a boolean?
print(classified["is_expired"] == "boolean")
# What is "order_service" classified as?
print(classified["order_service"])
# What name in the list is a class and contains "Service"?
service_class = [n for n, c in classified.items() if c == "class_name" and "Service" in n][0]
print(service_class)Solution
def classify_name(name: str) -> str:
if name == name.upper() and '_' in name:
return "constant"
if name[0].isupper():
return "class_name"
if any(name.startswith(p) for p in ("is_", "has_", "can_", "should_")):
return "boolean"
return "function_or_variable"
names = [
"MAX_CONNECTIONS",
"OrderService",
"order_service",
"calculate_tax",
"is_expired",
"has_premium",
"DEFAULT_TIMEOUT",
"API_BASE_URL",
"UserRepository",
]
classified = {n: classify_name(n) for n in names}
constants = [n for n, c in classified.items() if c == "constant"]
print(len(constants))
print(classified["order_service"] == "class_name")
print(classified["is_expired"] == "boolean")
print(classified["order_service"])
service_class = [n for n, c in classified.items() if c == "class_name" and "Service" in n][0]
print(service_class)
Output:
3
False
True
function_or_variable
OrderService
How it works: The classifier applies four rules in order. For each name:
MAX_CONNECTIONS—"MAX_CONNECTIONS" == "MAX_CONNECTIONS".upper()isTrueand it contains_→ constantDEFAULT_TIMEOUT— same test passes → constantAPI_BASE_URL— same test passes → constantOrderService— starts with uppercaseO→ class_nameUserRepository— starts with uppercaseU→ class_nameorder_service— lowercase start, no boolean prefix → function_or_variablecalculate_tax— same → function_or_variableis_expired— starts withis_→ booleanhas_premium— starts withhas_→ boolean
Three constants: MAX_CONNECTIONS, DEFAULT_TIMEOUT, API_BASE_URL → len(constants) = 3. classified["order_service"] == "class_name" is False (it is function_or_variable). classified["is_expired"] == "boolean" is True. The service class with "Service" in the name is "OrderService".
How the categories break down:
| Name | Category | Why |
|---|---|---|
MAX_CONNECTIONS | constant | All caps + underscore |
DEFAULT_TIMEOUT | constant | All caps + underscore |
API_BASE_URL | constant | All caps + underscore |
OrderService | class_name | Starts with uppercase |
UserRepository | class_name | Starts with uppercase |
order_service | function_or_variable | Lowercase, no boolean prefix |
calculate_tax | function_or_variable | Lowercase verb phrase |
is_expired | boolean | is_ prefix |
has_premium | boolean | has_ prefix |
Key insight: Python's naming conventions form a complete type inference system for human readers. Before reading a single line of logic, a reader scanning a module can determine: which names are constants (UPPER_SNAKE_CASE), which are classes (PascalCase), which are booleans (is_/has_/can_/should_), and which are functions or variables (snake_case verb phrases). This visual parsing is immediate and automatic for experienced Python developers — it is one of the most valuable conventions in the language.
Expected Output
3\nFalse\nTrue\norder_service\nOrderServiceHints
Hint 1: Constants should be UPPER_SNAKE_CASE. Class names should be PascalCase.
Hint 2: Boolean variables should use is_, has_, can_, or should_ prefixes.
Hint 3: Function names should be verb phrases in snake_case.
