10 Hidden Python Tricks Unveiled

10 Hidden Python Tricks Unveiled

Python is an excellent language for those who are starting out in the world of programming, but it has many details that can go unnoticed by beginners.

In this post, you will learn 10 tricks that every Python programmer should know to write better code!

01 - Swapping Variable Values Without Using Intermediaries

In many languages, swapping values between two variables requires a third temporary variable, for example in C:

#include <stdio.h>

int main() {
    int a = 5;
    int b = 10;
    int temp;

    temp = a;
    a = b;
    b = temp;

    printf("Output:\n");
    printf("a = %d\n", a); // a = 10
    printf("b = %d\n", b); // b = 5

    return 0;
}

In Python, we can do this elegantly:

x, y = 5, 10
x, y = y, x
print(x, y)  # Output: 10 5

This approach uses tuple destructuring to make swapping easier.

02 - PEP 8

PEP 8 is the official Python style guide. Following it improves readability and standardizes the code. Some important rules:

import os

def my_function():
    print("Following PEP 8!")

If you use VSCode, you can install the autopep8 extension to help you.

03 - Walrus Operator

The walrus operator, introduced in Python 3.8, allows assigning values within expressions, reducing code repetition:

# Without Walrus Operator
n_len = len([1, 2, 3, 4])
if n_len > 3:
    print(f"The list has {n_len} elements")

# With Walrus Operator
if (n := len([1, 2, 3, 4])) > 3:
    print(f"The list has {n} elements")

This operator is useful to avoid duplicating function calls (in this case, the len() function) and to improve code readability, especially in loops and conditionals.

Fun fact: This operator is named after its resemblance to a walrus's teeth (Walrus).

04 - Is Operator

Many people don't know this, but the is operator checks if two variables point to the same object in memory:

a = [1, 2, 3]
b = a
print(a is b)  # True

c = [1, 2, 3]
print(a is c)  # False

This operator is also commonly used to check if an object is None.

a = None
if a is None:
    print("a is None")

⚠️ Attention: To compare values, use ==, as is can lead to unexpected results with immutable types like strings and numbers due to interning:

x = 256
y = 256
print(x is y)  # True (small values can be interned)

05 - Copying objects

Assigning one variable to another (=) only creates a reference to the same object.

To create true copies, you need to use the copy library. Pay close attention, as there are 2 types of copies:

  • Shallow Copy: Does not copy nested objects, only their structure.

  • Deep Copy: Copies all objects recursively.

Let's see an example:

import copy

original_dict = {
    'a': 1,
    'b': [2, 3, 4],
    'c': {'x': 5, 'y': 6}
}

shallow_copy = copy.copy(original_dict)
deep_copy = copy.deepcopy(original_dict)

# Modifying the original object
original_dict['a'] = 100
original_dict['b'][0] = 200 # shallow_copy will have this value even if it is not modified \0/
original_dict['c']['x'] = 300

print("Original:", original_dict)
print("Shallow Copy:", shallow_copy)
print("Deep Copy:", deep_copy)

# Original: {'a': 100, 'b': [200, 3, 4], 'c': {'x': 300, 'y': 6}}
# Shallow Copy: {'a': 1, 'b': [200, 3, 4], 'c': {'x': 300, 'y': 6}} <--------------- 
# Deep Copy: {'a': 1, 'b': [2, 3, 4], 'c': {'x': 5, 'y': 6}}

In the example above, you can see that the property shallow_copy['b'][0] stores the value 200 even though it wasn't modified anywhere in the code.

In this case, since the copy is shallow, a reference to the original dictionary original_dict['b'] was copied to the property shallow_copy['b']. In other words, the properties original_dict['b'] and shallow_copy['b'] point to the same memory address.

On the other hand, the deep_copy dictionary was created completely independent of the others, with memory addresses that are not shared between the dictionaries.

Summary:

  • Shallow Copy: Copies only the structure but shares references to internal objects (lists, dictionaries, etc.).

  • Deep Copy: Copies everything recursively, creating independent objects.

06 - Negative Indexes

Python allows accessing elements of lists from the end using negative indices:

  • Last item: list[-1]

  • Second-to-last item: list[-2]

numbers = [10, 20, 30, 40]
print(numbers[-1])  # 40
print(numbers[-2])  # 30

07 - Monkey Patching

Monkey patching is the technique of modifying or replacing methods and attributes of existing classes at runtime.

It is useful for fixing bugs, modifying the behavior of third-party libraries, or adding functionality without altering the original source code, especially for testing purposes.

However, its use should be cautious, as it can introduce unexpected side effects.

class MathOperations: 

    def add(self, a, b) : 
        return a + b

    def subtract(self, a, b) : 
        return a - b 

# Define a new function to be added to the class 
def multiply(self, a, b): 
    return a * b 

MathOperations.multiply = multiply 

# Now, we can use the 'multiply' method as if it was part of the original class 
math_instance = MathOperations() 
result = math_instance.multiply(3, 4)

print("Result of multiplication: ", result) # Result of multiplication: 12

In the example above, an unintended side effect was ADDING a method to a class at runtime.

Imagine if the multiply method and the MathOperation class were in separate files. How long would it take you to realize that somewhere in the code there is a line that assigns a new method to a class?

08 - Timer context

Using context managers with with allows measuring the execution time of a code block in a convenient and automatic way.

This is useful for performance optimization, analyzing the execution time of critical functions, and debugging code bottlenecks.

import time

class Timer:
    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, *args):
        print(f"Execution time: {time.time() - self.start:.4f}s")

with Timer():
    sum(range(10**6))

# Execution time: 0.0194s

09 - Access specifiers

Python does not have strict access control like other languages, but it uses conventions to indicate the visibility of class attributes and methods:

  • _protected: Indicates that an attribute or method is protected and should only be used within the class and its subclasses.

  • __private: Indicates that an attribute or method should be private, but it can still be accessed using obj._Class__private (which I believe is justifiable to use ONLY in testing scenarios).

class MyClass:
    def __init__(self):
        self.public = "can be accessed"
        self._protected = "use with caution"
        self.__private = "should not be accessed directly"

obj = MyClass()
print(obj.public)  # OK
print(obj._protected)  # Convention: do not modify
# print(obj.__private)  # Error

10 - else in try/except

The else block inside a try/except allows separating code that should only execute when no exception occurs.

This improves organization and avoids unnecessary execution of code inside the try, reducing the risk of unexpected exceptions.

try:
    result = 10 / 2
except ZeroDivisionError:
    print("Error: Division by zero!")
else:
    print("Everything is fine! Result:", result)

Conclusion

These are just a few of the many details that make Python, the language of snakes, such a powerful and versatile language.

By mastering these techniques, you will be able to write more efficient, readable, and robust code.

Now, if you want to deepen your knowledge even further and learn best practices for writing much more professional code, I HIGHLY recommend the book: Robust Python. This book made me see Python in a whole new light!