DEV Community

Cover image for 5 Python good practices which make life easier
pikoTutorial
pikoTutorial

Posted on • Edited on • Originally published at pikotutorial.com

5 Python good practices which make life easier

Print exceptions traceback after catching them

Consider the following example code that raises an exception:

def send_request():
    raise RuntimeError('Send request timeout!')

def do_something():
    send_request()

def do_something_else():
    send_request()

do_something()
do_something_else()
Enter fullscreen mode Exit fullscreen mode

When we run it, we see the following output:

Traceback (most recent call last):
  File "<main.py>", line 11, in <module>
  File "<main.py>", line 5, in do_something
  File "<main.py>", line 2, in send_request
RuntimeError: Send request timeout!
Enter fullscreen mode Exit fullscreen mode

The traceback provides detailed information, including where the error occurred, the exception type, and the error message. However, the downside is that the program crashes without attempting to handle the error. This often leads developers to focus on implementing error handling without considering the need for debugging when errors persist. Let's add error handling to catch the exception:

try:
    do_something()
    do_something_else()
except RuntimeError as e:
    print(f'Operation failed: {e}')
Enter fullscreen mode Exit fullscreen mode

This code handles the exception gracefully, but the output is now:

Operation failed: Send request timeout!
Enter fullscreen mode Exit fullscreen mode

While this output is clean, it lacks the valuable traceback details we had before, making it harder to identify which function caused the error. It may be fine in some cases, but here it's especially problematic because send_request() function is used in more than one place in the code, so the exception could come either from do_something() or do_something_else() function. This information may be priceless during debugging and fortunately there's a way to bring it back without sacraficing program's robustness. Python provides a traceback module for that:

import traceback

try:
    do_something()
    do_something_else()
except RuntimeError as e:
    print(f'Operation failed: {e}\n' \
          f'{traceback.format_exc()}')
Enter fullscreen mode Exit fullscreen mode

Now, the output includes the full traceback, helping us pinpoint where the exception occurred:

Operation failed: Send request timeout!
Traceback (most recent call last):
  File "<main.py>", line 13, in <module>
  File "<main.py>", line 7, in do_something
  File "<main.py>", line 4, in send_request
RuntimeError: Send request timeout!
Enter fullscreen mode Exit fullscreen mode

This approach allows for effective error handling while maintaining critical debugging information.

Note for beginners: Some of you may now start asking: why bother with traceback module to obtain debugging information if just calling raise directly after printing error in except block will display them too? The thing is that in this example we don't want to exit the application after the exception has been thrown. Re-raising the exception will indeed preserve all the display information, but it will also terminate the program.

Avoid mutable default arguments

Using mutable default arguments (like lists or dictionaries) in function definitions can lead to unexpected behavior because they maintain their state across multiple function calls. Consider the following example:

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item(2))
print(add_item(3))
Enter fullscreen mode Exit fullscreen mode

You might expect each call to produce a single-element list, [2] and [3]. However, the actual output is:

[2]
[2, 3]
Enter fullscreen mode Exit fullscreen mode

This is what I meant by saying about retaining state across multiple function calls. The default value of items has changed from an empty list to a list with 2 inside added there during the first function call.


Read also on pikotutorial.com: Python lru_cache explained


Use virtual environments

Python's virtualenv or venv modules allow you to create isolated environments for your projects. This practice keeps dependencies required by different projects separate and prevents version conflicts. To create a virtual environment:

python -m venv myenv
Enter fullscreen mode Exit fullscreen mode

In order to use the environment, you need to first activate it in the currently used terminal. To do that on Windows, call:

myenv\Scripts\activate
Enter fullscreen mode Exit fullscreen mode

On Unix or MacOS:

source myenv/bin/activate
Enter fullscreen mode Exit fullscreen mode

From now on, you can use pip install as usual and whenever you want to save your project's dependencies, store the in a requirements.txt file suing command:

pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

Commit this file to your project's repository because it will allow everyone else to install all the necessary dependencies in their local virtual environments by calling:

pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Use enumerate for indexing

When iterating over a list and you need both the index and the value, use enumerate() instead of manually managing the index variable. This approach is more Pythonic and eliminates errors associated with manual index handling.

Instead of:

index = 0
for value in my_list:
    print(index, value)
    index += 1
Enter fullscreen mode Exit fullscreen mode

Use:

for index, value in enumerate(my_list):
    print(index, value)
Enter fullscreen mode Exit fullscreen mode

enumerate() is not only cleaner but also more readable, making your code less prone to bugs.

Use context managers for resource anagement

If you're familiar with RAII concept (Resource Acquisition Is Initialization), context managers may be treated as RAII implementation for Python. They ensure that resources like files, sockets or database connections are properly managed and automatically cleaned up after use. The most common example can be opening a file:

with open('file.txt', 'r') as f:
    content = f.read()
Enter fullscreen mode Exit fullscreen mode

After that, f is automatically closed at the end of with block.

Top comments (0)