Skip to main content Brad's PyNotes

PEP 20: The Zen of Python - Design Philosophy and Guiding Principles

TL;DR

PEP 20 presents the “Zen of Python” - 19 guiding principles that capture Python’s design philosophy, emphasizing readability, simplicity, and explicit over implicit approaches.

Interesting!

The Zen of Python is built into Python itself - you can access it anytime by typing import this in any Python interpreter, and it will display all 19 aphorisms.

The 19 Principles

python code snippet start

import this

python code snippet end

The Zen of Python, by Tim Peters:

  1. Beautiful is better than ugly.
  2. Explicit is better than implicit.
  3. Simple is better than complex.
  4. Complex is better than complicated.
  5. Flat is better than nested.
  6. Sparse is better than dense.
  7. Readability counts.
  8. Special cases aren’t special enough to break the rules.
  9. Although practicality beats purity.
  10. Errors should never pass silently.
  11. Unless explicitly silenced.
  12. In the face of ambiguity, refuse the temptation to guess.
  13. There should be one– and preferably only one –obvious way to do it.
  14. Although that way may not be obvious at first unless you’re Dutch.
  15. Now is better than never.
  16. Although never is often better than right now.
  17. If the implementation is hard to explain, it’s a bad idea.
  18. If the implementation is easy to explain, it may be a good idea.
  19. Namespaces are one honking great idea – let’s do more of those!

Every Principle in Practice

Let’s explore each of the 19 principles with concrete code examples:

1. Beautiful is better than ugly.

python code snippet start

# Beautiful - clean and elegant
users = [user for user in all_users if user.is_active]

# Ugly - verbose and awkward
users = []
for user in all_users:
    if user.is_active == True:
        users.append(user)

python code snippet end

2. Explicit is better than implicit.

python code snippet start

# Good - explicit behavior
def send_email(recipient, subject, body, send_immediately=False):
    email = Email(recipient, subject, body)
    if send_immediately:
        email.send()
    else:
        email.queue()

# Bad - implicit behavior
def send_email(recipient, subject, body):
    email = Email(recipient, subject, body)
    email.send()  # Always sends immediately - not obvious

python code snippet end

3. Simple is better than complex.

python code snippet start

# Simple
total = sum(numbers)

# Complex (unnecessarily)
total = reduce(lambda x, y: x + y, numbers, 0)

python code snippet end

4. Complex is better than complicated.

python code snippet start

# Complex but manageable - single responsibility
class UserManager:
    def __init__(self, db, cache, validator):
        self.db = db
        self.cache = cache 
        self.validator = validator
    
    def create_user(self, user_data):
        self.validator.validate(user_data)
        user = self.db.create(user_data)
        self.cache.invalidate('users')
        return user

# Complicated - tangled responsibilities
class UserManager:
    def create_user(self, user_data):
        # Validation logic mixed with persistence logic
        if not user_data.get('email') or '@' not in user_data['email']:
            raise ValueError("Invalid email")
        # Database logic mixed with caching logic
        with database.transaction() as tx:
            user_id = tx.execute("INSERT INTO users...")
            cache.delete(f"user:{user_id}")
            cache.delete("all_users")
            # More mixed concerns...

python code snippet end

5. Flat is better than nested.

python code snippet start

# Flat - early returns
def process_user(user):
    if not user:
        return None
    if not user.is_active:
        return None
    if not user.has_permission('read'):
        return None
    
    return user.get_data()

# Nested - pyramid of doom
def process_user(user):
    if user:
        if user.is_active:
            if user.has_permission('read'):
                return user.get_data()
    return None

python code snippet end

6. Sparse is better than dense.

python code snippet start

# Sparse - readable with whitespace
def calculate_score(base_score, multipliers):
    total = base_score
    
    for multiplier in multipliers:
        if multiplier > 0:
            total *= multiplier
    
    return min(total, MAX_SCORE)

# Dense - cramped and hard to read
def calculate_score(base_score, multipliers):
    total=base_score
    for multiplier in multipliers:
        if multiplier>0:total*=multiplier
    return min(total,MAX_SCORE)

python code snippet end

7. Readability counts.

python code snippet start

# Readable
user_is_authenticated = check_user_credentials(username, password)
if user_is_authenticated:
    grant_access_to_dashboard()

# Less readable
if check_user_credentials(username, password): grant_access_to_dashboard()

python code snippet end

8. Special cases aren’t special enough to break the rules.

python code snippet start

# Good - consistent interface
class FileHandler:
    def read(self, filename):
        with open(filename, 'r') as f:
            return f.read()

class DatabaseHandler:
    def read(self, query):
        return self.connection.execute(query).fetchall()

# Bad - breaking consistency for "special" case
class FileHandler:
    def read_file(self, filename):  # Different method name
        with open(filename, 'r') as f:
            return f.read()

class DatabaseHandler:
    def read(self, query):
        return self.connection.execute(query).fetchall()

python code snippet end

9. Although practicality beats purity.

python code snippet start

# Practical - works with real-world constraints
def get_user_data(user_id):
    # Check cache first for performance
    if user_id in cache:
        return cache[user_id]
    
    # Fall back to database
    user_data = database.get_user(user_id)
    cache[user_id] = user_data
    return user_data

# Pure but impractical - always hits database
def get_user_data(user_id):
    return database.get_user(user_id)  # Slow for repeated calls

python code snippet end

10. Errors should never pass silently.

python code snippet start

# Good - handle errors explicitly
def divide_numbers(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        logger.error(f"Division by zero: {a}/{b}")
        raise

# Bad - silent failure
def divide_numbers(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return None  # Silent failure - caller doesn't know what happened

python code snippet end

11. Unless explicitly silenced.

python code snippet start

# Explicitly silenced when appropriate
def optional_feature():
    try:
        import optional_dependency
        return optional_dependency.do_something()
    except ImportError:
        # Explicitly choosing to ignore this error
        logger.debug("Optional dependency not available, skipping feature")
        return None

python code snippet end

12. In the face of ambiguity, refuse the temptation to guess.

python code snippet start

# Good - explicit about ambiguous parameters
def create_user(name, email=None, username=None):
    if email is None and username is None:
        raise ValueError("Must provide either email or username")
    # Clear logic for each case...

# Bad - guessing what user wants
def create_user(identifier):
    # Guessing whether it's email or username
    if '@' in identifier:
        email = identifier  # Maybe wrong assumption
    else:
        username = identifier

python code snippet end

13. There should be one– and preferably only one –obvious way to do it.

python code snippet start

# Python's obvious way to iterate with index
for i, item in enumerate(items):
    print(f"{i}: {item}")

# Less obvious alternatives
for i in range(len(items)):  # More verbose
    print(f"{i}: {items[i]}")

i = 0  # Manual tracking
for item in items:
    print(f"{i}: {item}")
    i += 1

python code snippet end

14. Although that way may not be obvious at first unless you’re Dutch.

python code snippet start

# Python's slice notation - not obvious until you learn it
numbers = list(range(10))
evens = numbers[::2]     # Every second element
odds = numbers[1::2]     # Starting from index 1, every second
reversed_nums = numbers[::-1]  # Reverse

# This reference is to Guido van Rossum being Dutch - some Python 
# features that seem "obvious" to the language designer might not 
# be immediately obvious to everyone else!

python code snippet end

15. Now is better than never.

python code snippet start

# Good - implement basic functionality now
def backup_data():
    """Basic backup - can be improved later"""
    with open('backup.json', 'w') as f:
        json.dump(get_all_data(), f)

# Perfectionist paralysis - never shipping
# def backup_data():
#     """TODO: Implement perfect backup with compression, 
#     encryption, incremental backups, cloud storage, 
#     rollback capability..."""
#     pass  # Never gets implemented

python code snippet end

16. Although never is often better than right now.

python code snippet start

# Good - take time to think through the design
def process_payment(amount, card_info):
    # Wait! This needs careful consideration:
    # - Security implications
    # - Error handling
    # - Logging for audits
    # - Transaction rollback
    # Better to design this properly than rush it
    
    validate_card_info(card_info)
    with transaction():
        charge_result = payment_gateway.charge(amount, card_info)
        log_transaction(charge_result)
        return charge_result

# Bad - rushing critical functionality
def process_payment(amount, card_info):
    return payment_gateway.charge(amount, card_info)  # No validation, logging, etc.

python code snippet end

17. If the implementation is hard to explain, it’s a bad idea.

python code snippet start

# Hard to explain - bad idea
def mystery_function(data):
    return [x for x in [y[0] if isinstance(y, tuple) and len(y) > 0 
           else y for y in data] if x is not None and str(x).strip()]

# Easy to explain - good idea
def extract_valid_names(data):
    """Extract non-empty names from mixed data types."""
    names = []
    for item in data:
        # Extract first element if it's a tuple, otherwise use item directly
        name = item[0] if isinstance(item, tuple) and len(item) > 0 else item
        
        # Keep only non-empty names
        if name is not None and str(name).strip():
            names.append(name)
    
    return names

python code snippet end

18. If the implementation is easy to explain, it may be a good idea.

python code snippet start

# Easy to explain and understand
def calculate_tax(amount, tax_rate):
    """Calculate tax by multiplying amount by tax rate."""
    return amount * tax_rate

def format_currency(amount):
    """Format amount as currency with two decimal places."""
    return f"${amount:.2f}"

# Simple, clear, easy to explain = good ideas

python code snippet end

19. Namespaces are one honking great idea – let’s do more of those!

python code snippet start

# Good use of namespaces
import datetime
import json.encoder
from pathlib import Path

# Clear what comes from where
today = datetime.date.today()
encoder = json.encoder.JSONEncoder()
config_path = Path("config.json")

# Bad - polluting namespace
from datetime import *
from json import *
from pathlib import *

# Now unclear where things come from
today = date.today()  # Which date?

python code snippet end

The Philosophy in Action

These principles work together to create Python’s distinctive style. They guide not just how we write code, but how Python itself evolves. When you find yourself choosing between approaches, ask:

  • Is this explicit and readable?
  • Am I choosing simple over clever?
  • Does this follow established patterns?
  • Will other developers understand this easily?

The Zen of Python isn’t just poetry - it’s a practical framework for writing better, more maintainable code that embodies Python’s philosophy of clarity and elegance.

These philosophical principles work hand-in-hand with PEP 8's practical style guide to create the foundation of Pythonic code. The principles directly influence how we design function annotations and approach generator expressions for memory-efficient programming. Understanding these fundamentals helps you appreciate the design decisions behind PEP processes that shape Python’s evolution.

Reference: PEP 20 - The Zen of Python