Hidden Features of Python

Introduction

Python is a powerful programming language that's easy to learn and fun to play with. But beyond the basics, there are plenty of hidden features and tricks that can help you write more efficient and more effective Python code.

In this article, we'll uncover some of these hidden features and show you how to use them to improve your Python programming skills.

Exploring Python's Hidden Features

Python is full of hidden features, some of which are more hidden than others. These features can be incredibly useful and can help you write more efficient and readable code. However, they can also be a bit tricky to discover if you don't know where to look.

In the next few sections we'll take a look at a couple features that are both helpful to know and not known as well throughout Python programmers.

The _ (Underscore) in Python

One of Python's hidden gems is the underscore (_). It's a versatile character that can be used in various ways in Python code.

First, it can be used as a variable in Python. It's often used as a throwaway variable, i.e., a variable that is being declared but the value is not actually used.

for _ in range(5):
    print("Hello, World!")

In the above code, we're not using the variable _ anywhere, it's just there because a variable is required by the syntax of the for loop.

Second, _ is used for ignoring the specific values. If you don’t need the specific values or the values are not used, just assign the values to underscore.

# Ignore a value when unpacking
x, _, y = (1, 2, 3) # x = 1, y = 3

Here we need both the x and y variables, but Python's syntax won't let us declare them without something in betwee, so we use the underscore.

Last, in the Python console, _ represents the last executed expression value.

>>> 10 + 20
30
>>> _
30

Note: The use of _ for storing the last value is specific to Python’s interactive interpreter and won’t work in scripts!

Regex Debugging via Parse Tree

Regular expressions can be complex and hard to understand. Thankfully, Python provides a hidden feature to debug them via a parse tree. The re module in Python provides the re.DEBUG flag which can be used to debug regular expressions.

Consider the following code:

import re

re.compile("(\d+)\.(\d+)", re.DEBUG)

This will output:

SUBPATTERN 1 0 0
  MAX_REPEAT 1 MAXREPEAT
    IN
      CATEGORY CATEGORY_DIGIT
LITERAL 46
SUBPATTERN 2 0 0
  MAX_REPEAT 1 MAXREPEAT
    IN
      CATEGORY CATEGORY_DIGIT

 0. INFO 4 0b0 3 MAXREPEAT (to 5)
 5: MARK 0
 7. REPEAT_ONE 9 1 MAXREPEAT (to 17)
11.   IN 4 (to 16)
13.     CATEGORY UNI_DIGIT
15.     FAILURE
16:   SUCCESS
17: MARK 1
19. LITERAL 0x2e ('.')
21. MARK 2
23. REPEAT_ONE 9 1 MAXREPEAT (to 33)
27.   IN 4 (to 32)
29.     CATEGORY UNI_DIGIT
31.     FAILURE
32:   SUCCESS
33: MARK 3
35. SUCCESS
re.compile('(\\d+)\\.(\\d+)', re.DEBUG)

This is the parse tree of the regular expression. It shows that the regular expression has two subpatterns ((\d+) and (\d+)), separated by a literal dot (.).

This can be incredibly useful when debugging complex regular expressions. It gives you a clear, visual representation of your regular expression, showing exactly what each part of the expression does.

Note: The re.DEBUG flag does not work with the re.match() or re.search() functions. It only works with re.compile().

Ellipsis

Python's ellipsis is a unique feature that's not commonly seen in other programming languages. It's represented by three consecutive dots (...) and it's actually a built-in constant in Python. You might be wondering, what could this possibly be used for? Let's explore some of its applications.

Python's ellipsis can be used as a placeholder for code. This can be very useful when you're sketching out a program structure but haven't implemented all parts yet. For instance:

def my_func():
    ...
    # TODO: implement this function

Here, the ellipsis indicates that my_func is incomplete and needs to be implemented.

Python's ellipsis also plays a role in slicing multi-dimensional arrays, especially in data science libraries like NumPy. Here's how you can use it:

import numpy as np

# Create a 3D array
arr = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])

# Use ellipsis to access elements
print(arr[..., 2])

This will output:

[[ 3  6]
 [ 9 12]]

In this case, the ellipsis is used to access the third element of each sub-array in our 3D array.

The dir() Function

The dir() function is another hidden gem in Python. It's a powerful built-in function that returns a list of names in the current local scope or a list of attributes of an object.

When used without an argument, dir() returns a list of names in the current local scope. Here's an example:

x = 1
y = 2

print(dir())

This will print something like:

['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'x', 'y']

Here, x and y are the variables we defined, and the rest are built-in names in Python.

When used with an object as an argument, dir() returns a list of the object's attributes. For instance, if we use it with a string object:

print(dir('Hello, StackAbuse!'))

This will output:

['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

These are all the methods that you can use with a string object. dir() is a handy function when you want to explore Python objects and understand their capabilities.

Lambda Functions

Lambda functions, also known as anonymous functions, are a feature of Python that allow you to create small, one-time, unnamed functions that you can use quickly and then discard. They're perfect for when you need a function for a short period of time and don't want to bother with the full function definition syntax.

Here's how you would create a lambda function:

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

multiply = lambda x, y: x * y
print(multiply(5, 4))

Output:

20

In the above example, we've created a lambda function that multiplies two numbers together. We then call the function with the numbers 5 and 4, and it returns 20.

Note: While lambda functions are powerful and convenient, they should be used sparingly. Overuse of lambda functions can lead to code that is difficult to read and debug.

Chaining Comparison Operators

Python allows you to chain comparison operators in a way that's intuitive and easy to read. This can be a real time-saver when you're writing complex comparisons.

For example, let's say you want to check if a number is between 1 and 10. Instead of writing two separate comparisons and combining them with an and operator, you can do this:

x = 5
print(1 < x < 10)

Output:

True

In this example, 1 < x < 10 is equivalent to 1 < x and x < 10. Python checks both comparisons and returns True if both are true, just as if you'd used the and operator.

Note: You can chain as many comparisons as you want in this way. For example, 1 < x < 10 < x * 10 < 100 is perfectly valid Python code.

The zip() Function

Python's zip() function is a hidden gem that doesn't get the attention it deserves. This function, which has been part of Python since version 1.5, can make your code cleaner and more efficient by allowing you to iterate over multiple sequences in parallel.

Here's a simple example of how it works:

names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]

for name, age in zip(names, ages):
    print(f"{name} is {age} years old.")

And the output:

Alice is 25 years old.
Bob is 30 years old.
Charlie is 35 years old.

Note: The zip() function stops at the end of the shortest input sequence. So if your sequences aren't the same length, no exception is raised - but you may lose some data from the longer sequences.

Decorators

Decorators are another powerful feature of Python that can greatly simplify your code. Essentially, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.

Let's look at an example. Suppose you have a function that performs an operation, but you want to log when the operation starts and ends. You could add logging statements directly to the function, but that would clutter your code. Instead, you can create a decorator to handle the logging:

def log_decorator(func):
    def wrapper():
        print("Starting operation...")
        func()
        print("Operation finished.")
    return wrapper

@log_decorator
def perform_operation():
    print("Performing operation...")

perform_operation()

When you run this code, you'll see the following output:

Starting operation...
Performing operation...
Operation finished.

The @log_decorator line is a decorator. It tells Python to pass the function perform_operation() to the log_decorator() function. The log_decorator() function then wraps the perform_operation() function with additional code to log the start and end of the operation.

Note: Decorators can also take arguments, which allows them to be even more flexible. Just remember that if your decorator takes arguments, you need to write it as a function that returns a decorator, rather than a simple decorator function.

Context Managers and the "with" Statement

In Python, context managers are a hidden gem that can be super useful in managing resources. They allow you to allocate and release resources precisely when you want to. The most widely used example of context managers is the with statement.

Let's take a look at an example:

with open('hello.txt', 'w') as f:
    f.write('Hello, World!')

In this example, the with statement is used to open a file and assign it to the variable f. The file is kept open for the duration of the with block, and automatically closed at the end, even if exceptions occur within the block. This ensures that the clean-up is done for us.

Note: Using the with statement is like saying, "with this thing, do this stuff, and no matter how it ends, close it out properly."

Generators and the Yield Statement

Generators are a type of iterable, like lists or tuples. They do not allow indexing but they can still be iterated through with for loops. They are created using functions and the yield statement.

The yield statement is used to define a generator, replacing the return of a function to provide a result to its caller without destroying local variables.

Here's a simple generator that generates even numbers:

def even_numbers(n):
    for i in range(n):
        if i % 2 == 0:
            yield i

for number in even_numbers(10):
    print(number)

Output:

0
2
4
6
8

Unlike normal functions, the local variables are not destroyed when the function yields. Furthermore, the generator object can be iterated only once.

Note: Generators are a great way to produce data which is huge or infinite. It represents a stream of data; this feature is used in Python 3's range() function.

These hidden features of Python, context managers and generators, can make your code more efficient and readable. They are worth understanding and using in your day-to-day Python coding.

Metaclasses

In Python, everything is an object - including classes themselves. This fact leads us to the concept of metaclasses. A metaclass is the class of a class; a class is an instance of a metaclass. It's a higher-level abstraction that controls the creation and management of classes in Python.

To define a metaclass, we typically inherit from the built-in type class. Let's take a look at a simple example:

class Meta(type):
    def __new__(cls, name, bases, attrs):
        print(f"Creating a new class named: {name}")
        return super().__new__(cls, name, bases, attrs)

class MyClass(metaclass=Meta):
    pass

Running this code, you'll see the following output:

$ python3 metaclass_example.py
Creating a new class named: MyClass

In the above example, Meta is our metaclass that inherits from type. It overrides the __new__ method, which is responsible for creating and returning a new object. When we define MyClass with Meta as its metaclass, the __new__ method of Meta is called, and we see our custom message printed.

Note: Metaclasses can be powerful, but they can also make code more complex and harder to understand. Use them sparingly and only when necessary.

Conclusion

Python is a versatile language with a plethora of hidden features that can make your coding experience more efficient and enjoyable. From the often overlooked underscore, to the powerful and complex concept of metaclasses, there's always something new to discover in Python. The key to mastering these features is understanding when and how to use them appropriately in your code.

Last Updated: August 28th, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms