Menu
7 Python Anti Patterns to Avoid

7 Python Anti Patterns to Avoid

Tech With Tim

18,150 views 8 months ago Save 14 min 7 min read

Video Summary

This video explores seven common Python anti-patterns that developers frequently encounter. It highlights how seemingly simple syntax can lead to performance issues and bugs, especially when the underlying time complexity is not understood. Key anti-patterns include inefficient list and string concatenation, incorrect use of equality versus identity checks (double equals vs. is), neglecting list comprehensions for better performance, not utilizing context managers for resource management, favoring print statements over the logging module for debugging, overlooking the Python standard library, and the critical pitfall of using mutable default arguments in function definitions. A striking example of an anti-pattern involves repeatedly appending to a list that serves as a default argument, causing unexpected state persistence across function calls.

One particularly insidious anti-pattern involves mutable default arguments in function definitions. Instead of creating a new list or dictionary each time a function is called with a default value, the default mutable object persists and is modified with each subsequent call. This can lead to bizarre and difficult-to-debug behavior where the function's output unexpectedly changes based on prior invocations. For instance, a function intended to add an item to a list might, on its second call, return a list containing both the original and the new item, even if the original list was not explicitly passed.

Short Highlights

  • Inefficient operations like list concatenation (+) and string concatenation can lead to O(n^2) time complexity instead of O(n) when using methods like .append() or .join().
  • Use the is operator for identity checks (e.g., is None, is True, is False) instead of the == operator to avoid potential issues with overridden __eq__ methods.
  • List comprehensions, dictionary comprehensions, and generator expressions offer significantly better performance than standard for loops for creating collections.
  • Context managers (using the with statement) are crucial for safe and automatic resource management, such as file handling, preventing memory leaks and data corruption.
  • The logging module should be preferred over print statements for production code, offering better control over log levels, formatting, and output destinations (console or files).
  • Ignoring Python's rich standard library (e.g., collections.Counter, os.path, pathlib) leads to reinventing the wheel and less efficient code.
  • Mutable default arguments in function definitions (e.g., def func(default_list=[])) cause unexpected behavior as the default object persists and is modified across calls; use None as a default and initialize inside the function.

Key Details

Understanding Time Complexity and Python Syntax [00:35]

  • Python's convenient syntax for operations like list concatenation, string concatenation, and method calls (e.g., .count()) can hide significant performance implications.
  • Developers must understand time complexity, often represented by Big O notation, to evaluate the efficiency of operations, especially in the context of growing data sizes.
  • Operations like list.append() or using .join() for strings are generally more efficient (O(n)) than repeated concatenation, which can degrade to O(n^2) due to the creation of new objects with each operation.
  • Checking for membership (in or not in) in lists can be O(n), whereas converting to a set first allows for O(1) average time complexity for these checks.

"The point is just because Python has this syntax doesn't mean you have to use it."

Equality vs. Identity: == vs. is [05:30]

  • The == operator checks for equality by calling the __eq__ method, while the is operator checks for object identity (whether two variables point to the exact same object in memory).
  • For comparisons involving None, True, and False, it is more Pythonic and safer to use the is operator (e.g., if my_var is None:) as it directly checks identity.
  • Using == for these comparisons can lead to unexpected behavior if custom classes override the __eq__ method, potentially returning True when identity is actually what's desired.
  • When checking for emptiness (e.g., empty lists or strings), directly using the boolean evaluation of the object (e.g., if not my_list:) is more idiomatic than comparing with [] or "".

"The main reason why we want to avoid this is because we end up calling this underscore equal equal method which can be overridden by your custom classes which could actually allow a comparison to return true when you don't want it to."

Leveraging List Comprehensions and Generator Expressions [08:20]

  • List comprehensions, dictionary comprehensions, and generator expressions are often more performant than traditional for loops for creating collections.
  • These constructs offer a more concise and Pythonic way to generate lists, dictionaries, and sets, especially when filtering or transforming data.
  • While readability is important, the performance benefits of comprehensions are particularly noticeable when dealing with large datasets.
  • Generator expressions provide a memory-efficient way to iterate over sequences, producing items on demand rather than creating a full list in memory.

"A lot of people will debate with me in the comments on this, but it does have better performance, especially when you're working with large amount of values."

The Power of Context Managers with Statement [10:50]

  • Context managers, invoked using the with statement, automate the setup and teardown of resources, ensuring cleanup operations (like closing files) occur even if errors arise.
  • This pattern prevents common issues such as resource leaks (e.g., unclosed files) and data corruption, which can occur if manual cleanup fails due to exceptions.
  • The with statement guarantees that the __exit__ method of the context manager is called, regardless of whether the block completes successfully or an exception is raised.
  • Python's built-in functions like open() act as context managers, simplifying file operations significantly compared to manual try...finally blocks.

"Even if an error were to occur in here and we were to like raise an exception or something, it will still be fine because we have the context manager, it will automatically clean up any of the operations that are defined in what's called the exit method."

Prioritizing Logging Over Print Statements [13:55]

  • Using print statements for debugging and information output in production code is an anti-pattern due to its lack of control over log levels, timestamps, and output destinations.
  • The logging module provides a robust framework for managing logs, allowing developers to categorize messages (DEBUG, INFO, WARNING, ERROR), format them consistently, and direct them to various handlers (console, files, network).
  • Properly configured logging makes it easier to diagnose issues, track application behavior over time, and manage output in different environments.
  • The logging module's ability to log to files is particularly valuable for post-mortem analysis and auditing.

"The issue is you're not going to know what file it's coming from, what time it's happening at, or what level of log it is, whether it's a debug, whether it's an info, whether it's a warning, whether it's an error, etc."

Embracing the Python Standard Library [16:25]

  • Many developers unknowingly write custom code for tasks already efficiently handled by Python's extensive standard library modules.
  • Ignoring built-in modules like collections (for Counter), os.path, and pathlib can lead to less optimized, more verbose, and potentially buggier code.
  • Familiarity with standard library tools for common tasks (e.g., path manipulation, data aggregation, file system operations) saves development time and promotes code reliability.
  • While memorizing the entire library isn't necessary, understanding common modules for frequently encountered problems is highly beneficial.

"Python has a lot of those features that are pretty well-known. So, if possible, just use them. It will save you a lot of time and you will just have better code."

The Pitfall of Mutable Default Arguments [18:34]

  • Using mutable objects (like lists, dictionaries, sets) as default arguments in function definitions is a significant anti-pattern.
  • The default mutable object is created only once when the function is defined and is reused across all subsequent calls that do not provide an explicit argument.
  • This reuse means that modifications made to the default object within the function persist across calls, leading to unexpected and hard-to-debug behavior.
  • The correct approach is to use None as the default value and then initialize the mutable object within the function body if the argument is None, ensuring a fresh object for each call.

"It doesn't work like you would expect it to work. If you modify this parameter right here from inside of the function, it actually changes the value of the default that will be used on a subsequent function call."

Other People Also See

Why cholesterol doesn't matter
Why cholesterol doesn't matter
William Davis , MD 22,534 views Save 8 min 4 min read
Top 5 WORST Bread Brands To Avoid
Top 5 WORST Bread Brands To Avoid
Bobby Parrish 701,557 views Save 15 min 6 min read
Top 5 WORST Butter Brands To Avoid
Top 5 WORST Butter Brands To Avoid
Bobby Parrish 745,543 views Save 14 min 5 min read