Back to Posts
Computer screens displaying code with the text 'GOTCHA!' highlighting Python common pitfalls and fixes for syntactic snafus in programming.

Fixing Common Syntactic Snafus and Pitfalls in Python

By Alyce Osbourne

While Python is well known for being easy to learn and read, it isn’t without its quirks. There are a few syntactic snafus that can really ruin your day if they find their way into your project!

Funky Floats

Comparing floating-point numbers in Python can be tricky due to the inherent imprecision of floating-point arithmetic. This imprecision can lead to unexpected results in comparisons.

Example

print(.1 + .2 == .3)  # False

The result is False because the sum of .1 and .2 is not exactly .3 due to floating-point rounding errors. These small inaccuracies can cause subtle bugs in your code.

Fix

import math
print(math.isclose(.1 + .2, .3))  # True
print(math.isclose(.1 + .2, .3, rel_tol=1e-9))  # True
print(math.isclose(.1 + .2, .3, abs_tol=1e-9))  # True

import decimal
print(decimal.Decimal('.1') + decimal.Decimal('.2') == decimal.Decimal('.3'))  # True

To address this issue, you can use the math.isclose function, which allows you to specify a relative or absolute tolerance for the comparison. This helps account for small floating-point inaccuracies:

  • math.isclose(a, b): This checks if the values a and b are close to each other, considering a default relative tolerance.
  • math.isclose(a, b, rel_tol=1e-9): Here, a relative tolerance of 1e-9 is specified, making the comparison stricter.
  • math.isclose(a, b, abs_tol=1e-9): This uses an absolute tolerance, ensuring the comparison considers the absolute difference between the values.

Alternatively, you can use the decimal module for arbitrary precision arithmetic, which provides exact decimal representation and comparison:

  • decimal.Decimal('value'): Creates a Decimal object with the specified value. This ensures precise arithmetic operations and comparisons. Note, the value passed needs to be a string representation of the float value in question.

Shifty Scopes

Understanding the scope of variables can occasionally be rather unintuitive. Sometimes we expect a variable or argument to behave one way, and instead it behaves in another.

Example

squares = [lambda x: x * i for i in range(5)]
print([f(2) for f in squares])  # [8, 8, 8, 8, 8], not [0, 2, 4, 6, 8]

We expect the i variable to be unique for each entry in the list, but instead we find that it was the last variable of the loop that was captured.

Fix

To fix this, you can use a default argument in the lambda function to capture the current value of i:

squares = [lambda x, i=i: x * i for i in range(5)]
print([f(2) for f in squares])  # [0, 2, 4, 6, 8]

This ensures that each lambda function retains the value of i at the time of its creation. By specifying i=i in the lambda function, we create a default parameter that holds the value of i at the time of the lambda’s definition, thus avoiding the reference issue. This binds the current iterations variable to the scope of the function. This gotcha can manifest itself in many places, but the most common is within loops.

Mutable Madness

Likely the most common gotcha on this list, assigning mutable variables to arguments.

Using mutable default arguments in functions can lead to unexpected behaviour, as the default argument is only evaluated once when the function is defined, not each time the function is called. This means that the mutable value is shared amongst instances.

Example

def add_to_list(value, my_list=[]):
    my_list.append(value)
    return my_list

print(add_to_list(1))  # [1]
print(add_to_list(2))  # [1, 2], not [2]

The list my_list is shared across all calls to add_to_list, leading to the accumulation of values.

Fix

To avoid this issue, use None as the default value and create a new list inside the function if needed:

def add_to_list(value, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(value)
    return my_list

print(add_to_list(1))  # [1]
print(add_to_list(2))  # [2]

This way, each call to add_to_list gets a new list if one is not provided. By using my_list=None and checking if my_list is None, you ensure that a new list is created for each function call, avoiding unintended accumulation.

Murky Matching

Structural pattern matching is an incredibly useful tool, but there are a few little things that behave in ways you might not initially expect.

A common one is matching values against types.

Example

match 10:
    case int:
        print("It's an int")
    case _:
        print("It's something else!")

# It's something else!

It might not seem immediately obvious what the error is here. The problem is, we are comparing an instance with a type, which is much like 10 == int.

Fix

To have the pattern matching work the way we intend, we adjust our syntax:

match 10:
    case int():
        print("It's an int")
    case _:
        print("It's something else!")

# It's an int

When we use int() in the pattern matching, it helps us correctly recognize that the value 10 is an integer, much like calling isinstance(10, int). This makes sure that the pattern matcher understands the type and compares the value accordingly, instead of treating the type itself as a value to compare directly. The same principle can be applied to other types, such as floats, lists, dicts, etc., by using float(), list(), dict() respectively.

Final thoughts

While most of the time Python is simple and straightforward, there are language quirks that can trip up beginner and expert alike with their seemingly unintuitive behaviour. Thankfully, the Python development team is improving the language all of the time, if I had written this list a few years ago, this list would have been much longer.

Improve your code with my 3-part code diagnosis framework

Watch my free 30 minutes code diagnosis workshop on how to quickly detect problems in your code and review your code more effectively.

When you sign up, you'll get an email from me regularly with additional free content. You can unsubscribe at any time.

Recent posts