| # A Concise, Opinionated Guide to Writing Good Code (with Python examples) | |
| This guide summarizes core principles for writing clean, maintainable, and effective code. It's opinionated and rule-based, designed to provide clear direction for junior developers. Adhering to these rules will help you build better software and become a more valuable team member. Python examples are provided for clarity. | |
| ## 1. Naming Matters Immensely | |
| * **Rule:** Use intention-revealing names. | |
| * **Don't:** `d = (datetime.now() - start_date).days` | |
| * **Do:** `elapsed_time_in_days = (datetime.now() - start_date).days` | |
| * **Rule:** Avoid disinformation. | |
| * **Don't:** `account_list = {"id": 1, "name": "Alice"}` (It's a dictionary, not a list) | |
| * **Do:** `account_data = {"id": 1, "name": "Alice"}` or `account_dict = ...` | |
| * **Rule:** Use pronounceable and searchable names. | |
| * **Don't:** `genymdhms = datetime.now().strftime('%Y%m%d%H%M%S')` | |
| * **Do:** `generation_timestamp = datetime.now().strftime('%Y%m%d%H%M%S')` | |
| * **Rule:** Be consistent. | |
| * **Don't:** Using `fetch_user_data`, `getUserInfo`, `retrieve_client_details` in the same project. | |
| * **Do:** Consistently use one style, e.g., `get_user_data`, `get_order_info`, `get_product_details`. | |
| ## 2. Functions Should Be Small and Focused | |
| * **Rule:** Functions must do **one thing**. | |
| * **Don't:** | |
| ```python | |
| def process_user_data(user_id): | |
| # Fetches data | |
| response = requests.get(f"/api/users/{user_id}") | |
| user_data = response.json() | |
| # Validates data | |
| if not user_data.get("email"): | |
| raise ValueError("Email missing") | |
| # Saves data | |
| db.save(user_data) | |
| # Sends notification | |
| send_email(user_data["email"], "Welcome!") | |
| return user_data | |
| ``` | |
| * **Do:** Break it down: | |
| ```python | |
| def fetch_user_data(user_id): | |
| response = requests.get(f"/api/users/{user_id}") | |
| response.raise_for_status() # Raise HTTP errors | |
| return response.json() | |
| def validate_user_data(user_data): | |
| if not user_data.get("email"): | |
| raise ValueError("Email missing") | |
| # ... other validations | |
| def save_user_data(user_data): | |
| db.save(user_data) | |
| def send_welcome_email(email_address): | |
| send_email(email_address, "Welcome!") | |
| def register_user(user_id): | |
| user_data = fetch_user_data(user_id) | |
| validate_user_data(user_data) | |
| save_user_data(user_data) | |
| send_welcome_email(user_data["email"]) | |
| return user_data | |
| ``` | |
| * **Rule:** Functions must be **small**. (The "Do" example above also illustrates this). | |
| * **Rule:** Minimize function arguments. | |
| * **Don't:** `def create_user(name, email, password, dob, address, phone, role, is_active): ...` | |
| * **Do:** | |
| ```python | |
| class UserProfile: | |
| def __init__(self, name, email, dob, address, phone): | |
| # ... initialization ... | |
| def create_user(profile: UserProfile, password: str, role: str, is_active: bool = True): | |
| # ... use profile attributes ... | |
| ``` | |
| Or pass a dictionary: | |
| ```python | |
| def create_user(user_details: dict): | |
| # Access details via user_details['name'], user_details['email'] etc. | |
| # Consider using TypedDict for better structure if using Python 3.8+ | |
| ... | |
| ``` | |
| * **Rule:** Avoid side effects where possible. | |
| * **Don't (Hidden Side Effect):** | |
| ```python | |
| user_list = [] | |
| def add_user_if_valid(name, email): | |
| if "@" in email: | |
| user_list.append({"name": name, "email": email}) # Modifies global state | |
| return True | |
| return False | |
| ``` | |
| * **Do (Explicit):** | |
| ```python | |
| def create_user_record(name, email): | |
| if "@" not in email: | |
| raise ValueError("Invalid email") | |
| return {"name": name, "email": email} | |
| # Usage | |
| try: | |
| new_user = create_user_record("Bob", "bob@example.com") | |
| user_list.append(new_user) # State change happens outside the function | |
| except ValueError as e: | |
| print(f"Error: {e}") | |
| ``` | |
| ## 3. Comments Are for "Why," Not "What" | |
| * **Rule:** Comment the "Why," not the "What." | |
| * **Don't:** | |
| ```python | |
| # Check if user is eligible | |
| if age >= 18 and country == "US": # This just repeats the code | |
| is_eligible = True | |
| ``` | |
| * **Do:** | |
| ```python | |
| # User must be a legal adult in the US to qualify for this specific offer. | |
| if age >= 18 and country == "US": | |
| is_eligible = True | |
| ``` | |
| * **Rule:** Do **not** leave commented-out code. | |
| * **Don't:** | |
| ```python | |
| def calculate_total(items): | |
| total = 0 | |
| for item in items: | |
| total += item['price'] | |
| # tax = total * 0.10 # Old tax calculation | |
| # total += tax | |
| total *= 1.10 # Apply 10% tax | |
| return total | |
| ``` | |
| * **Do:** Remove the commented lines. Use Git history if you need to see the old calculation. | |
| ```python | |
| def calculate_total(items): | |
| total = sum(item['price'] for item in items) | |
| total *= 1.10 # Apply 10% tax | |
| return total | |
| ``` | |
| * **Rule:** Keep comments up-to-date. (Self-explanatory - if the logic changes, update or remove the comment). | |
| * **Rule:** Avoid redundant comments. | |
| * **Don't:** | |
| ```python | |
| count = 0 # Initialize count | |
| count += 1 # Increment count | |
| ``` | |
| * **Do:** Just the code is enough. | |
| ```python | |
| count = 0 | |
| count += 1 | |
| ``` | |
| ## 4. Formatting and Structure Enhance Readability | |
| * **Rule:** Use a consistent style guide (e.g., PEP 8 for Python). Use tools like `Black`, `Flake8`, `isort`. | |
| * **Don't:** Inconsistent spacing, line lengths, import orders. | |
| * **Do:** Code automatically formatted by tools like `Black`. | |
| * **Rule:** Top-down narrative. | |
| * **Don't:** Define helper functions *before* the main function that uses them, forcing the reader to jump around. | |
| * **Do:** | |
| ```python | |
| def main_process(): | |
| data = _fetch_data() | |
| result = _process_data(data) | |
| _save_result(result) | |
| # --- Helper functions defined below --- | |
| def _fetch_data(): ... | |
| def _process_data(data): ... | |
| def _save_result(result): ... | |
| ``` | |
| *(Note: Leading underscore `_` often indicates internal/helper functions)* | |
| * **Rule:** Keep related concepts vertically close. (The example above also shows this). | |
| * **Rule:** Use whitespace. | |
| * **Don't:** | |
| ```python | |
| def process(a,b,c): | |
| x=a+b | |
| y=x*c | |
| if y>10: | |
| print("Large") | |
| else: | |
| print("Small") | |
| z=y-a | |
| return z | |
| ``` | |
| * **Do:** | |
| ```python | |
| def process(a, b, c): | |
| intermediate_value = a + b | |
| final_value = intermediate_value * c | |
| if final_value > 10: | |
| print("Large") | |
| else: | |
| print("Small") | |
| adjusted_value = final_value - a | |
| return adjusted_value | |
| ``` | |
| ## 5. Keep It Simple (KISS & YAGNI) | |
| * **Rule:** KISS (Keep It Simple, Stupid). | |
| * **Don't:** Using complex metaprogramming or obscure language features when a simple loop or conditional would suffice. | |
| * **Do:** Prefer straightforward, readable solutions. | |
| * **Rule:** YAGNI (You Ain't Gonna Need It). | |
| * **Don't:** Adding configuration options, database fields, or API endpoints for features that *might* be needed in the future but aren't required now. | |
| * **Do:** Implement only what's necessary for the current requirements. | |
| * **Rule:** Avoid premature optimization. | |
| * **Don't:** Spending hours micro-optimizing a function with string concatenations before profiling to see if it's even a bottleneck. | |
| * **Do:** Write clean code first. If performance is an issue (measure it!), profile and optimize the specific hotspots. Often, a better algorithm beats micro-optimization. | |
| ## 6. Don't Repeat Yourself (DRY) | |
| * **Rule:** Avoid duplication. | |
| * **Don't:** | |
| ```python | |
| def process_file_a(path): | |
| # 10 lines of validation logic | |
| if not valid: return None | |
| # Process file A specific logic | |
| ... | |
| def process_file_b(path): | |
| # Same 10 lines of validation logic copied here | |
| if not valid: return None | |
| # Process file B specific logic | |
| ... | |
| ``` | |
| * **Do:** | |
| ```python | |
| def _validate_input(path): | |
| # 10 lines of validation logic | |
| return is_valid | |
| def process_file_a(path): | |
| if not _validate_input(path): return None | |
| # Process file A specific logic | |
| ... | |
| def process_file_b(path): | |
| if not _validate_input(path): return None | |
| # Process file B specific logic | |
| ... | |
| ``` | |
| ## 7. Handle Errors Gracefully | |
| * **Rule:** Use exceptions over error codes. | |
| * **Don't:** | |
| ```python | |
| def divide(a, b): | |
| if b == 0: | |
| return -1 # Error code | |
| return a / b | |
| result = divide(10, 0) | |
| if result == -1: | |
| print("Error: Division by zero") | |
| ``` | |
| * **Do:** | |
| ```python | |
| def divide(a, b): | |
| if b == 0: | |
| raise ValueError("Cannot divide by zero") | |
| return a / b | |
| try: | |
| result = divide(10, 0) | |
| except ValueError as e: | |
| print(f"Error: {e}") | |
| ``` | |
| * **Rule:** Provide context with errors. | |
| * **Don't:** `raise Exception("Error!")` | |
| * **Do:** `raise ValueError(f"Invalid user ID format: '{user_id_str}'")` | |
| ## 8. Test Your Code | |
| * **Rule:** Write unit tests (using frameworks like `pytest` or `unittest`). | |
| * **Don't:** Skipping tests because the code "looks simple." | |
| * **Do:** | |
| ```python | |
| # Example using pytest | |
| from my_module import add | |
| def test_add_positive_numbers(): | |
| assert add(2, 3) == 5 | |
| def test_add_negative_numbers(): | |
| assert add(-1, -1) == -2 | |
| def test_add_mixed_numbers(): | |
| assert add(5, -3) == 2 | |
| ``` | |
| * **Rule:** Test behavior, not implementation. | |
| * **Don't:** Writing a test that checks if a specific private helper method (`_helper`) was called. | |
| * **Do:** Writing a test that checks if the public method produces the correct output or state change, regardless of which internal helpers were used. | |
| * **Rule:** Keep tests clean, readable, and fast. (Apply the same principles from this guide to your test code). | |
| ## 9. Practice Continuous Refactoring | |
| * **Rule:** Follow the Boy Scout Rule. | |
| * **Don't:** Seeing a poorly named variable or a slightly complex block of code and leaving it because "it works." | |
| * **Do:** Taking a few moments to rename the variable or extract a small function to improve clarity before committing your primary change. | |
| * **Rule:** Refactoring is part of development. (This is a mindset, less about specific code examples). | |
| ## 10. Optimize for Readability | |
| * **Rule:** Code is read more than written. | |
| * **Don't:** Using overly clever one-liners or complex list comprehensions that are hard to decipher. | |
| ```python | |
| # Clever but potentially hard to read | |
| result = [x**2 for x in range(10) if x % 2 == 0 and x > 3] | |
| ``` | |
| * **Do:** Prioritize clarity, even if it means slightly more verbose code. | |
| ```python | |
| result = [] | |
| for x in range(10): | |
| is_even = x % 2 == 0 | |
| is_greater_than_3 = x > 3 | |
| if is_even and is_greater_than_3: | |
| result.append(x**2) | |
| # Or a more readable comprehension if appropriate | |
| result = [x**2 for x in range(4, 10, 2)] # Clearer range | |
| ``` | |
| ## 11. Python-Specific Best Practices | |
| * **Rule:** Embrace Pythonic idioms. | |
| * **Use List Comprehensions (when clear):** Prefer `squares = [x*x for x in numbers]` over manual `for` loop appends for simple transformations. | |
| * **Use Context Managers (`with` statement):** Ensure resources like files or network connections are properly closed. | |
| ```python | |
| # Don't | |
| f = open("myfile.txt", "w") | |
| try: | |
| f.write("Hello") | |
| finally: | |
| f.close() | |
| # Do | |
| with open("myfile.txt", "w") as f: | |
| f.write("Hello") | |
| # File is automatically closed here, even if errors occur | |
| ``` | |
| * **Iterate Directly:** Iterate over sequences directly instead of using index manipulation. | |
| ```python | |
| # Don't | |
| for i in range(len(my_list)): | |
| print(my_list[i]) | |
| # Do | |
| for item in my_list: | |
| print(item) | |
| # Do (if index is needed) | |
| for i, item in enumerate(my_list): | |
| print(f"Index {i}: {item}") | |
| ``` | |
| * **Rule:** Use Type Hinting (Python 3.5+). Improves readability, enables static analysis tools (`mypy`), and clarifies intent. | |
| ```python | |
| # Don't | |
| def greet(name): | |
| print("Hello " + name) | |
| # Do | |
| def greet(name: str) -> None: | |
| print("Hello " + name) | |
| def add(a: int, b: int) -> int: | |
| return a + b | |
| ``` | |
| * **Rule:** Use Virtual Environments (`venv`). Isolate project dependencies to avoid conflicts between projects. Always create and activate a virtual environment before installing packages (`pip install ...`). | |
| * **Rule:** Prefer f-strings (Python 3.6+) for string formatting. They are generally more readable and often faster than `.format()` or `%` formatting. | |
| ```python | |
| name = "Alice" | |
| age = 30 | |
| # Don't (older styles) | |
| print("Name: %s, Age: %d" % (name, age)) | |
| print("Name: {}, Age: {}".format(name, age)) | |
| # Do | |
| print(f"Name: {name}, Age: {age}") | |
| ``` | |
| * **Rule:** Understand Mutable Default Arguments. Be wary of using mutable types (like lists or dicts) as default function arguments, as they are shared across calls. | |
| ```python | |
| # Don't (potential bug) | |
| def add_item(item, my_list=[]): | |
| my_list.append(item) | |
| return my_list | |
| list1 = add_item(1) # [1] | |
| list2 = add_item(2) # [1, 2] - Unexpected! | |
| # Do | |
| def add_item(item, my_list=None): | |
| if my_list is None: | |
| my_list = [] | |
| my_list.append(item) | |
| return my_list | |
| list1 = add_item(1) # [1] | |
| list2 = add_item(2) # [2] - Correct | |