Skip to main content

Python Naming Conventions Practice Problems & Exercises

Practice: Naming Conventions

11 problems4 Easy4 Medium3 Hard40-55 min
← Back to lesson

Easy

#1snake_case: Predict the Convention ViolationsEasy
snake_casenamingpep8

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.

Python
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\nFalse
Hints

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.

#2PascalCase: Class Names vs Variable NamesEasy
PascalCaseclassesnaming

Predict the output. Four names are checked against a PascalCase validator. Understand which naming patterns Python classes should follow.

Python
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\nFalse
Hints

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.

#3UPPER_SNAKE_CASE: Spotting ConstantsEasy
constantsUPPER_SNAKE_CASEFinal

Predict the output. Module constants are defined and accessed. One uses typing.Final for stricter enforcement.

Python
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\nTrue
Hints

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.

#4Boolean Naming: is_, has_, can_, should_Easy
boolean-namingpredicatesis_has_can_should_

Predict the output. Boolean variables are assigned following the naming conventions from the lesson. Trace the logic.

Python
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\nTrue
Hints

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

#5_private Convention: What It Does and Doesn't DoMedium
_privatesingle-underscorename-manglingconvention

Predict the output. A class has a public method and a private helper. Explore whether Python actually restricts access to _private members.

Python
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\nTrue
Hints

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.

#6__name_mangling: How Double Underscore Actually WorksMedium
__manglingdouble-underscoresubclassname-mangling

Predict the output. Name mangling with double underscores ensures subclass attributes do not collide with parent attributes of the same name.

Python
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\nTrue
Hints

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.

#7Dunder Methods: Implementing __repr__, __eq__, __lt__Medium
dunder__repr____eq____lt__magic-methods

Predict the output. A Temperature class implements three dunder methods. Trace each operation.

Python
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\nTrue
Hints

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().

#8Meaningful Naming: Refactor the Unreadable FunctionMedium
meaningful-namesrefactoringdomain-languagereadability

Predict the output. Two functions compute the same result. The first uses single-letter names; the second uses domain names. Verify they are equivalent.

Python
# 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\nTrue
Hints

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

#9Name Mangling Deep Dive: Attribute Access PatternsHard
__manglingvars__dict__attribute-access

Predict the output. Explore how name mangling affects the attribute dictionary and external access patterns.

Python
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\nAttributeError
Hints

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.

#10Class Design: Avoiding the Manager/Helper Suffix TrapHard
class-namingsingle-responsibilityPascalCasedesign

Predict the output. Three focused classes replace a bloated UserManager. Trace the interactions.

Python
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()

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)

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!\nAlice
Hints

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.

#11Naming Audit: Classify and Fix a Complete ModuleHard
naming-auditsnake_casePascalCaseconstantsbooleansrefactoring

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.

Python
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() is True and it contains _constant
  • DEFAULT_TIMEOUT — same test passes → constant
  • API_BASE_URL — same test passes → constant
  • OrderService — starts with uppercase Oclass_name
  • UserRepository — starts with uppercase Uclass_name
  • order_service — lowercase start, no boolean prefix → function_or_variable
  • calculate_tax — same → function_or_variable
  • is_expired — starts with is_boolean
  • has_premium — starts with has_boolean

Three constants: MAX_CONNECTIONS, DEFAULT_TIMEOUT, API_BASE_URLlen(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:

NameCategoryWhy
MAX_CONNECTIONSconstantAll caps + underscore
DEFAULT_TIMEOUTconstantAll caps + underscore
API_BASE_URLconstantAll caps + underscore
OrderServiceclass_nameStarts with uppercase
UserRepositoryclass_nameStarts with uppercase
order_servicefunction_or_variableLowercase, no boolean prefix
calculate_taxfunction_or_variableLowercase verb phrase
is_expiredbooleanis_ prefix
has_premiumbooleanhas_ 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\nOrderService
Hints

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.

© 2026 EngineersOfAI. All rights reserved.