Skip to main content
โšก Calmops

Python Naming Conventions and PEP 8: Writing Pythonic Code

Introduction

Have you ever opened a Python file and immediately felt confused by inconsistent naming? One function uses camelCase, another uses snake_case. Variables have cryptic single-letter names. Classes are named like functions. This inconsistency makes code harder to read, understand, and maintain.

PEP 8 (Python Enhancement Proposal 8) is the official style guide for Python code. It establishes conventions for naming, formatting, and organizing code that the entire Python community follows. By adhering to these conventions, you write code that’s not just correctโ€”it’s Pythonic.

In this guide, we’ll explore PEP 8 naming conventions, understand why they matter, and learn how to apply them consistently in your projects.


What Is PEP 8?

The Importance of Style Guides

PEP 8 is Python’s official style guide, created by Guido van Rossum (Python’s creator) and other core developers. It provides guidelines for:

  • Naming conventions - How to name variables, functions, classes, etc.
  • Code formatting - Indentation, line length, whitespace
  • Comments and docstrings - How to document code
  • Programming recommendations - Best practices and patterns

The key principle behind PEP 8 is stated in the Zen of Python:

“Readability counts.”

Code is read far more often than it’s written. By following consistent conventions, you make your code accessible to other developers (and your future self).

Why Follow PEP 8?

โœ… Consistency - Everyone’s code looks similar
โœ… Readability - Easier to understand and maintain
โœ… Collaboration - Teams work more efficiently
โœ… Professional - Industry standard for Python projects
โœ… Tooling - Linters and formatters expect PEP 8


Naming Conventions Overview

Python uses different naming styles for different constructs:

Construct Convention Example
Variables snake_case user_name, total_price
Functions snake_case calculate_total(), get_user()
Classes PascalCase UserManager, DataProcessor
Constants UPPER_CASE MAX_ATTEMPTS, API_KEY
Modules snake_case user_manager.py, data_processor.py
Packages snake_case user_management, data_processing

Variables: snake_case

The Rule

Variable names should use snake_case: lowercase letters with underscores separating words.

# Good - clear and readable
user_name = "Alice"
total_price = 99.99
is_active = True
max_attempts = 3

# Avoid - camelCase (used in other languages)
userName = "Alice"
totalPrice = 99.99
isActive = True

# Avoid - unclear abbreviations
un = "Alice"
tp = 99.99
ia = True

Why snake_case?

  • Readability - Underscores are easier to read than camelCase
  • Consistency - Python standard across the ecosystem
  • Accessibility - Better for screen readers and dyslexic developers

Real-World Example

# E-commerce system
class ShoppingCart:
    def __init__(self):
        self.items = []
        self.total_price = 0.0
        self.discount_percentage = 0
        self.is_checkout_complete = False
    
    def add_item(self, item_name: str, item_price: float) -> None:
        self.items.append({"name": item_name, "price": item_price})
        self.total_price += item_price
    
    def apply_discount(self, discount_percentage: float) -> None:
        self.discount_percentage = discount_percentage
        self.total_price *= (1 - discount_percentage / 100)

Functions: snake_case

The Rule

Function names should use snake_case, with verbs that describe what the function does.

# Good - descriptive verbs
def calculate_total(items):
    pass

def get_user_by_id(user_id):
    pass

def is_valid_email(email):
    pass

def send_notification(user_id, message):
    pass

# Avoid - unclear names
def calc(items):
    pass

def get(user_id):
    pass

def check(email):
    pass

def notify(user_id, msg):
    pass

Naming Patterns

Getters - Use get_ prefix:

def get_user(user_id):
    return users[user_id]

def get_total_price(items):
    return sum(item["price"] for item in items)

Setters - Use set_ prefix:

def set_user_email(user_id, email):
    users[user_id]["email"] = email

def set_discount(percentage):
    global discount
    discount = percentage

Checkers - Use is_ or has_ prefix:

def is_valid_email(email):
    return "@" in email

def has_permission(user, action):
    return action in user["permissions"]

def is_empty(collection):
    return len(collection) == 0

Converters - Use to_ prefix:

def to_uppercase(text):
    return text.upper()

def to_json(data):
    return json.dumps(data)

def to_list(generator):
    return list(generator)

Classes: PascalCase

The Rule

Class names should use PascalCase (also called CapWords): capitalize the first letter of each word, no underscores.

# Good - PascalCase
class UserManager:
    pass

class DataProcessor:
    pass

class HTTPClient:
    pass

class XMLParser:
    pass

# Avoid - snake_case
class user_manager:
    pass

class data_processor:
    pass

# Avoid - camelCase
class userManager:
    pass

class dataProcessor:
    pass

Why PascalCase for Classes?

  • Distinction - Immediately identifies classes vs. functions
  • Convention - Standard across Python and many languages
  • Clarity - Makes code more scannable

Real-World Example

class UserAuthentication:
    """Handles user login and authentication."""
    
    def __init__(self, database):
        self.database = database
        self.session_timeout = 3600
    
    def authenticate_user(self, username: str, password: str) -> bool:
        user = self.database.get_user(username)
        return user and self._verify_password(password, user["password_hash"])
    
    def _verify_password(self, password: str, hash_value: str) -> bool:
        # Implementation details
        pass

class EmailNotification:
    """Sends email notifications to users."""
    
    def __init__(self, smtp_server: str):
        self.smtp_server = smtp_server
    
    def send_welcome_email(self, user_email: str, user_name: str) -> None:
        subject = f"Welcome, {user_name}!"
        body = f"Hello {user_name}, welcome to our service!"
        self._send_email(user_email, subject, body)
    
    def _send_email(self, recipient: str, subject: str, body: str) -> None:
        # Implementation details
        pass

Constants: UPPER_CASE

The Rule

Constants (values that don’t change) should use UPPER_CASE with underscores separating words.

# Good - clearly indicates constants
MAX_ATTEMPTS = 3
API_KEY = "your-api-key"
DATABASE_URL = "postgresql://localhost/mydb"
TIMEOUT_SECONDS = 30
DEFAULT_LANGUAGE = "en"

# Avoid - looks like a variable
max_attempts = 3
api_key = "your-api-key"
database_url = "postgresql://localhost/mydb"

Important Note

Python doesn’t enforce constantsโ€”they’re just a convention. You can still modify them, but the UPPER_CASE name signals “don’t change this.”

# Configuration constants
class Config:
    MAX_UPLOAD_SIZE = 10 * 1024 * 1024  # 10 MB
    ALLOWED_EXTENSIONS = {".jpg", ".png", ".gif"}
    CACHE_EXPIRY_MINUTES = 60
    RETRY_ATTEMPTS = 3
    
    # Database configuration
    DB_HOST = "localhost"
    DB_PORT = 5432
    DB_NAME = "production_db"

Private and Protected Names

Single Underscore: Protected

A single leading underscore indicates a name is for internal use:

class DataProcessor:
    def __init__(self):
        self._internal_state = {}
        self._cache = []
    
    def process(self, data):
        """Public method."""
        return self._process_internal(data)
    
    def _process_internal(self, data):
        """Protected method - for internal use."""
        # Implementation
        pass

Double Underscore: Private

Double leading underscores trigger name mangling, making the name harder to access:

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # Private
    
    def deposit(self, amount):
        """Public method."""
        self.__update_balance(amount)
    
    def __update_balance(self, amount):
        """Private method - name mangled to _BankAccount__update_balance."""
        self.__balance += amount
    
    def get_balance(self):
        return self.__balance

# Usage
account = BankAccount(1000)
account.deposit(500)
print(account.get_balance())  # Output: 1500

# Accessing private members (possible but discouraged)
# print(account._BankAccount__balance)  # Works but bad practice

When to Use Each

  • No underscore - Public API, intended for external use
  • Single underscore - Internal implementation, use with caution
  • Double underscore - Rarely needed; use only to prevent name conflicts in subclasses

Special Methods: Dunder Names

What Are Dunder Methods?

“Dunder” methods (double underscore) are special methods that Python calls automatically. They’re always written with double underscores on both sides:

class Product:
    def __init__(self, name: str, price: float):
        """Constructor - called when creating an instance."""
        self.name = name
        self.price = price
    
    def __str__(self) -> str:
        """String representation for users."""
        return f"Product: {self.name} (${self.price})"
    
    def __repr__(self) -> str:
        """String representation for developers."""
        return f"Product(name='{self.name}', price={self.price})"
    
    def __eq__(self, other) -> bool:
        """Equality comparison."""
        return self.name == other.name and self.price == other.price
    
    def __lt__(self, other) -> bool:
        """Less than comparison."""
        return self.price < other.price
    
    def __len__(self) -> int:
        """Length of the product name."""
        return len(self.name)

# Usage
product1 = Product("Laptop", 999.99)
product2 = Product("Mouse", 29.99)

print(str(product1))      # Output: Product: Laptop ($999.99)
print(repr(product1))     # Output: Product(name='Laptop', price=999.99)
print(product1 == product2)  # Output: False
print(product2 < product1)   # Output: True
print(len(product1))      # Output: 6

Modules and Packages

Module Names

Module names (Python files) should use snake_case:

# Good
user_manager.py
data_processor.py
email_service.py
api_client.py

# Avoid
UserManager.py
DataProcessor.py
EmailService.py
APIClient.py

Package Names

Package names (directories) should use snake_case:

# Good directory structure
my_project/
โ”œโ”€โ”€ user_management/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ models.py
โ”‚   โ”œโ”€โ”€ services.py
โ”‚   โ””โ”€โ”€ utils.py
โ”œโ”€โ”€ data_processing/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ processors.py
โ”‚   โ””โ”€โ”€ validators.py
โ””โ”€โ”€ api_client/
    โ”œโ”€โ”€ __init__.py
    โ””โ”€โ”€ client.py

# Avoid
my_project/
โ”œโ”€โ”€ UserManagement/
โ”œโ”€โ”€ DataProcessing/
โ””โ”€โ”€ APIClient/

Common Naming Pitfalls

Pitfall 1: Single-Letter Variables

# Avoid - unclear what x, y, z represent
def calculate(x, y, z):
    return x + y * z

# Better - descriptive names
def calculate(base_amount, tax_rate, discount):
    return base_amount + base_amount * tax_rate * discount

Pitfall 2: Ambiguous Abbreviations

# Avoid - unclear abbreviations
def proc_usr_data(ud):
    return ud

# Better - clear names
def process_user_data(user_data):
    return user_data

Pitfall 3: Misleading Names

# Avoid - name doesn't match what it does
def get_user_count():
    """This actually returns user data, not a count!"""
    return fetch_all_users()

# Better - accurate names
def get_all_users():
    return fetch_all_users()

def get_user_count():
    return len(fetch_all_users())

Pitfall 4: Inconsistent Naming

# Avoid - inconsistent patterns
class UserManager:
    def get_user(self, user_id):
        pass
    
    def create_user(self, name):
        pass
    
    def user_exists(self, user_id):  # Inconsistent pattern
        pass
    
    def remove_user(self, user_id):
        pass

# Better - consistent patterns
class UserManager:
    def get_user(self, user_id):
        pass
    
    def create_user(self, name):
        pass
    
    def has_user(self, user_id):  # Consistent with is_/has_ pattern
        pass
    
    def delete_user(self, user_id):
        pass

Tools for Checking PEP 8 Compliance

flake8

A popular linter that checks PEP 8 compliance:

# Install
pip install flake8

# Check a file
flake8 myfile.py

# Check a directory
flake8 src/

pylint

A comprehensive code analyzer:

# Install
pip install pylint

# Check a file
pylint myfile.py

black

An opinionated code formatter that enforces PEP 8:

# Install
pip install black

# Format a file
black myfile.py

# Format a directory
black src/

Example: Using flake8

$ flake8 example.py
example.py:1:1: E302 expected 2 blank lines, found 0
example.py:5:1: E501 line too long (88 > 79 characters)
example.py:10:1: F841 local variable 'unused_var' is assigned to but never used

Conclusion

Following Python naming conventions and PEP 8 guidelines is essential for writing professional, maintainable code. Here are the key takeaways:

Remember:

  1. Variables and functions use snake_case
  2. Classes use PascalCase
  3. Constants use UPPER_CASE
  4. Private members use _single_underscore or __double_underscore
  5. Modules and packages use snake_case
  6. Be descriptive - Names should clearly indicate purpose
  7. Be consistent - Follow the same patterns throughout your codebase
  8. Use tools - Let flake8, pylint, or black help enforce standards

By adhering to these conventions, you write code that’s not just correctโ€”it’s Pythonic. Your teammates will appreciate the consistency, and your future self will thank you for the clarity.

Start applying these conventions in your next project, and you’ll quickly develop the habit of writing clean, readable Python code.

Happy coding! ๐Ÿ


Quick Reference

# Variables and functions
user_name = "Alice"
total_price = 99.99

def calculate_total(items):
    pass

# Classes
class UserManager:
    pass

# Constants
MAX_ATTEMPTS = 3
API_KEY = "secret"

# Private/Protected
class MyClass:
    def __init__(self):
        self._protected = "internal"
        self.__private = "very internal"
    
    def _protected_method(self):
        pass
    
    def __private_method(self):
        pass

# Modules
# user_manager.py
# data_processor.py

# Packages
# user_management/
# data_processing/

Comments