Python Tricks Roundup 2

By: Cam Wohlfeil
Published: 2019-06-17 1045 EDT
Modified: 2019-06-17 1715 EDT
Category: Programming
Tags: python

I subscribed to Dan Bader's mailing lists in January so you don't have to (you should though, it's decent). Here's the second set of tricks I got from it so far.

Get the name of an object's class as a string:

class MyClass: pass
obj = MyClass()
obj.__class__.__name__
# 'MyClass'

# Functions have a similar feature:
def myfunc(): pass
myfunc.__name__
# 'myfunc'

Check for class inheritance relationships with the "issubclass()" built-in:

class BaseClass: pass
class SubClass(BaseClass): pass
issubclass(SubClass, BaseClass)
# True
issubclass(SubClass, object)
# True
issubclass(BaseClass, SubClass)
# False

Python 3 allows unicode variable names:

π = math.pi
class Spin̈alTap: pass
Spin̈alTap()
# <Spin̈alTap object at 0x10e58d908>

Only letter-like characters work, however:

🍺 = "beer"
# SyntaxError:
#  "invalid character in identifier"

"globals()" returns a dict with all global variables in the current scope:

globals()
# {...}

locals() does the same but for all local variables in the current scope:

locals()
# {...}

Python 3.3+ has a stdlib module for displaying tracebacks, even when Python "dies":

import faulthandler
faulthandler.enable()

Can also be enabled with "python -X faulthandler" from the command line.

Learn more here: https://docs.python.org/3/library/faulthandler.html

Avoid version conflicts with Virtual Environments

Virtual Environments ("virtualenvs") keep your project dependencies separated. They help you avoid version conflicts between packages and different versions of the Python runtime.

Before creating & activating a virtualenv:python and pip map to the systemversion of the Python interpreter (e.g. Python 2.7)

$ which python
# /usr/local/bin/python

Let's create a fresh virtualenv using another version of Python (Python 3):

$ python3 -m venv ./venv

A virtualenv is just a "Python environment in a folder":

$ ls ./venv
# bin      include    lib      pyvenv.cfg

Activating a virtualenv configures the current shell session to use the python (and pip) commands from the virtualenvfolder instead of the global environment:

$ source ./venv/bin/activate

Note how activating a virtualenv modifies your shell prompt with a little note showing the name of the virtualenv folder:

(venv) $ echo "wee!"

With an active virtualenv, the python command maps to the interpreter binary inside the active virtualenv:

(venv) $ which python
# /Users/dan/my-project/venv/bin/python3

Installing new libraries and frameworks with pip now installs them into the virtualenv sandbox, leaving your global environment (and any other virtualenvs) completely unmodified:

(venv) $ pip install requests

To get back to the global Python environment, run the following command:

(venv) $ deactivate

(See how the prompt changed back to "normal" again?)

$ echo "yay!"

Deactivating the virtualenv flipped the python and pip commands back to the global environment:

$ which python
# /usr/local/bin/python

Python's for and while loops support an else clause that executes only if the loops terminates without hitting a break statement:

def contains(haystack, needle):
    """
    Throw a ValueError if `needle` not
    in `haystack`.
    """
    for item in haystack:
        if item == needle:
            break
    else:
        # The `else` here is a
        # "completion clause" that runs
        # only if the loop ran to completion
        # without hitting a `break` statement.
        raise ValueError('Needle not found')


contains([23, 'needle', 0xbadc0ffee], 'needle')
# None

contains([23, 42, 0xbadc0ffee], 'needle')
# ValueError: "Needle not found"

But I'd rather do something like this:

def better_contains(haystack, needle):
    for item in haystack:
        if item == needle:
            return
    raise ValueError('Needle not found')

Note: Typically you'd write something like this to do a membership test, which is much more Pythonic:

if needle not in haystack:
    raise ValueError('Needle not found')

Pythonic ways of checking if all items in a list are equal:

lst = ['a', 'a', 'a']
len(set(lst)) == 1
# True
all(x == lst[0] for x in lst)
# True
lst.count(lst[0]) == len(lst)
# True

I ordered those from "most Pythonic" to "least Pythonic" and "least efficient" to "most efficient". The len(set()) solution is idiomatic, but constructing a set is less efficient memory and speed-wise.

In Python 3.4+ you can use contextlib.suppress() to selectively ignore exceptions:

import contextlib

with contextlib.suppress(FileNotFoundError):
    os.remove('somefile.tmp')

# This is equivalent to:

try:
    os.remove('somefile.tmp')
except FileNotFoundError:
    pass

contextlib.suppress docstring:

"Return a context manager that suppresses any 
 of the specified exceptions if they occur in the body
 of a with statement and then resumes execution with 
 the first statement following the end of 
 the with statement."

In Python 3 you can use an asterisk in function parameter lists to force the caller to use keyword arguments:

def f(a, b, *, c='x', d='y', e='z'):
    return 'Hello'

To pass the value for c, d, and e you will need to explicitly pass it as "key=value" named arguments:

f(1, 2, 'p', 'q', 'v')
# TypeError: 
#  "f() takes 2 positional arguments but 5 were given"

f(1, 2, c='p', d='q',e='v')
# 'Hello'

Python 3.5+ allows passing multiple sets of kwargs to a function using the "**" syntax:

def process_data(a, b, c, d):
   print(a, b, c, d)

x = {'a': 1, 'b': 2}
y = {'c': 3, 'd': 4}

process_data(**x, **y)
# 1 2 3 4

process_data(**x, c=23, d=42)
# 1 2 23 42