tylersmith1985
tylersmith1985 1d ago • 0 views

Common mistakes when using functions and procedures in Python

Hey everyone! 👋 I'm really trying to get better at Python, especially when it comes to organizing my code with functions and procedures. I keep running into weird errors or my code doesn't behave the way I expect, and it's super frustrating. What are some of the most common pitfalls people fall into? I want to avoid them! Any insights would be amazing. Thanks! 🙏
💻 Computer Science & Technology
🪄

🚀 Can't Find Your Exact Topic?

Let our AI Worksheet Generator create custom study notes, online quizzes, and printable PDFs in seconds. 100% Free!

✨ Generate Custom Content

1 Answers

✅ Best Answer
User Avatar
allen.justin40 Mar 22, 2026

📚 Understanding Python Functions & Procedures: A Foundation

Python functions and procedures are fundamental building blocks for organizing code, promoting reusability, and enhancing readability. They allow developers to encapsulate specific tasks, making complex programs manageable. While Python doesn't strictly differentiate between "functions" (which return a value) and "procedures" (which perform an action but don't explicitly return a value, implicitly returning None), the terms are often used interchangeably to refer to callable blocks of code.

📜 A Brief History of Modular Programming

The concept of breaking down programs into smaller, manageable units dates back to the early days of computing. As programs grew in complexity, the need for modularity became evident. Subroutines, procedures, and functions emerged as essential tools for structured programming, allowing code to be written once and invoked multiple times. Python, with its emphasis on readability and simplicity, adopted and refined these concepts, making them central to its design philosophy.

⚠️ Common Pitfalls When Using Functions & Procedures in Python

  • 🚫 Misunderstanding Variable Scope: Local vs. Global

    A frequent error involves confusion between local and global variables. Variables defined inside a function are local to that function and cannot be directly accessed or modified outside it, unless explicitly declared global using the global keyword (which is generally discouraged for good practice).

    # Mistake: Trying to modify a global variable directly
    x = 10
    def modify_x_bad():
        x = 5 # This creates a new local variable 'x'
        print(f"Inside bad function: x = {x}")
    modify_x_bad() # Output: Inside bad function: x = 5
    print(f"Outside after bad function: x = {x}") # Output: Outside after bad function: x = 10 (global x is unchanged)
    
    # Correct approach: Pass as argument or explicitly declare global (use with caution)
    y = 10
    def modify_y_good(val):
        return val + 5
    y = modify_y_good(y)
    print(f"Outside after good function: y = {y}") # Output: Outside after good function: y = 15
  • 🧪 Mutable Default Arguments: A Hidden Trap

    Using mutable objects (like lists, dictionaries, or sets) as default argument values can lead to unexpected behavior. The default argument is created only once when the function is defined, not each time the function is called. Subsequent calls without providing an argument will reuse the same mutable object, accumulating changes.

    # Mistake: Mutable default argument
    def add_item_bad(item, item_list=[]):
        item_list.append(item)
        return item_list
    print(add_item_bad('apple')) # Output: ['apple']
    print(add_item_bad('banana')) # Output: ['apple', 'banana'] - unexpected!
    
    # Correct approach: Use None as default and initialize inside the function
    def add_item_good(item, item_list=None):
        if item_list is None:
            item_list = []
        item_list.append(item)
        return item_list
    print(add_item_good('apple')) # Output: ['apple']
    print(add_item_good('banana')) # Output: ['banana'] - correct!
  • 🔄 Ignoring or Misunderstanding Return Values

    Functions are often expected to return a result. Forgetting to return a value, or not capturing the returned value, can lead to None type errors or incorrect program flow. If a function doesn't explicitly return a value, it implicitly returns None.

    # Mistake: Not returning a value
    def calculate_sum_bad(a, b):
        total = a + b
    # print(calculate_sum_bad(2, 3)) # Output: None
    
    # Correct approach: Always return the intended result
    def calculate_sum_good(a, b):
        total = a + b
        return total
    result = calculate_sum_good(2, 3)
    print(result) # Output: 5
  • ⚙️ Unintended Side Effects

    A function has a "side effect" if it modifies some state outside its local scope (e.g., modifying a global variable, printing to console, writing to a file, or changing a mutable argument passed to it). While sometimes necessary, excessive or poorly managed side effects make code harder to understand, test, and debug.

    # Mistake: Unintended side effect on a global list
    my_list = [1, 2, 3]
    def modify_list_bad(lst):
        lst.append(4) # Modifies the original list
    modify_list_bad(my_list)
    print(my_list) # Output: [1, 2, 3, 4] - original list was changed
    
    # Better approach: Return a new list or explicitly acknowledge modification
    my_list_2 = [10, 20, 30]
    def add_to_list_good(lst, item):
        new_list = list(lst) # Create a copy
        new_list.append(item)
        return new_list
    new_modified_list = add_to_list_good(my_list_2, 40)
    print(my_list_2) # Output: [10, 20, 30] - original list unchanged
    print(new_modified_list) # Output: [10, 20, 30, 40]
  • 🔢 Incorrect Parameter Passing and Argument Mismatches

    Python uses "pass-by-object-reference." This means that when you pass an argument to a function, you're passing a reference to the object. For immutable objects (numbers, strings, tuples), changes inside the function don't affect the original object outside. For mutable objects (lists, dictionaries), changes do affect the original object. Also, common mistakes include passing the wrong number or type of arguments.

    # Mistake: Argument count mismatch
    # def greet(name):
    #     print(f"Hello, {name}!")
    # greet() # TypeError: greet() missing 1 required positional argument: 'name'
    
    # Mistake: Type mismatch (runtime error if not handled)
    # def add_numbers(a, b):
    #     return a + b
    # add_numbers(5, "hello") # TypeError: unsupported operand type(s) for +: 'int' and 'str'
    
    # Correct: Ensure correct number and types of arguments
    def greet_person(name):
        print(f"Greetings, {name}!")
    greet_person("Alice")
    
    def safe_add_numbers(a, b):
        if isinstance(a, (int, float)) and isinstance(b, (int, float)):
            return a + b
        else:
            raise TypeError("Both arguments must be numbers.")
    print(safe_add_numbers(5, 10))
  • 📝 Lack of Documentation (Docstrings)

    Functions without clear documentation (docstrings) are harder to understand, use, and maintain, especially in larger projects or when collaborating. Docstrings explain what the function does, its arguments, and what it returns.

    # Mistake: No docstring
    def multiply(a, b):
        return a * b
    
    # Correct: Add a clear docstring
    def multiply_values(a, b):
        """
        Multiplies two numbers and returns their product.
    
        Args:
            a (int or float): The first number.
            b (int or float): The second number.
    
        Returns:
            int or float: The product of a and b.
        """
        return a * b
    print(multiply_values.__doc__)
  • 🏗️ Violating the Single Responsibility Principle (SRP)

    A function should ideally do one thing and do it well. Functions that try to accomplish too many tasks become complex, difficult to test, and less reusable. Break down complex logic into smaller, focused functions.

    # Mistake: Function doing too many things
    def process_user_data_bad(user_id, data):
        # 1. Validate data
        if not isinstance(data, dict):
            raise ValueError("Data must be a dictionary.")
        # 2. Save to database
        # db.save(user_id, data)
        # 3. Send confirmation email
        # send_email(user_id, "Data updated")
        # 4. Log the action
        # logger.info(f"User {user_id} data updated.")
        return "Processed"
    
    # Better: Break into smaller, focused functions
    def validate_data(data):
        if not isinstance(data, dict):
            raise ValueError("Data must be a dictionary.")
        return True
    
    def save_to_database(user_id, data):
        # db.save(user_id, data)
        print(f"Saving data for {user_id}...")
    
    def send_confirmation_email(user_id, message):
        # send_email(user_id, message)
        print(f"Sending email to {user_id} with message: {message}")
    
    def log_action(user_id, action):
        # logger.info(f"User {user_id} {action}.")
        print(f"Logging: User {user_id} {action}.")
    
    def process_user_data_good(user_id, data):
        validate_data(data)
        save_to_database(user_id, data)
        send_confirmation_email(user_id, "Data updated successfully.")
        log_action(user_id, "data updated")
        return "Processed successfully"
  • 🚨 Ignoring Exception Handling

    Functions that interact with external resources (files, network) or receive user input can encounter errors. Failing to handle these exceptions gracefully can lead to program crashes and poor user experience. Use try-except blocks where appropriate.

    # Mistake: No exception handling
    # def read_file_bad(filepath):
    #     with open(filepath, 'r') as f:
    #         return f.read()
    # read_file_bad("non_existent_file.txt") # FileNotFoundError
    
    # Correct: Implement exception handling
    def read_file_good(filepath):
        try:
            with open(filepath, 'r') as f:
                return f.read()
        except FileNotFoundError:
            print(f"Error: File '{filepath}' not found.")
            return None
        except IOError as e:
            print(f"Error reading file '{filepath}': {e}")
            return None
    print(read_file_good("non_existent_file.txt"))

💡 Best Practices for Robust Python Functions

  • 🎯 Clarity & Purpose: Design functions to perform a single, well-defined task.
  • 🧊 Immutability Preference: Favor immutable arguments and return new objects instead of modifying existing ones when possible.
  • ➡️ Explicit Returns: Always explicitly return values where expected; avoid relying on implicit None returns for core logic.
  • 📖 Docstrings & Type Hints: Document your functions thoroughly with docstrings and use type hints (e.g., def func(param: str) -> int:) for better readability and maintainability.
  • Testing: Write unit tests for your functions to ensure they work as expected and catch regressions.
  • 🚧 Error Handling: Implement robust error handling using try-except blocks to gracefully manage potential issues.
  • ♻️ Avoid Global State: Minimize reliance on global variables; pass necessary data as arguments.

🚀 Conclusion: Mastering Function Design

Understanding and avoiding these common mistakes will significantly improve the quality, reliability, and maintainability of your Python code. By adopting best practices in function design, you'll write more robust, readable, and efficient programs, paving the way for advanced software development.

Join the discussion

Please log in to post your answer.

Log In

Earn 2 Points for answering. If your answer is selected as the best, you'll get +20 Points! 🚀