folio / docs /coding-guidelines.md
dystomachina's picture
refactor(data-fetcher): simplify data fetcher to use only YFinance
203d1e9

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:
      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:
      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:
      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:
      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):
      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):
      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:
      # Check if user is eligible
      if age >= 18 and country == "US": # This just repeats the code
          is_eligible = True
      
    • Do:
      # 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:
      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.
      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:
      count = 0 # Initialize count
      count += 1 # Increment count
      
    • Do: Just the code is enough.
      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:
      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:
      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:
      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:
      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:
      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:
      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:
      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:
      # 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.
      # 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.
      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.
      # 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.
      # 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.
    # 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.
    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.
    # 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