Image by Author | Ideogram
Have you ever discovered a feature in a tool you’ve used for years and thought, “How did I miss this?” Yes, and today we’ll try to do a “Python functions” version of that.
These functions are likely part of your daily code already, but I bet you’re not getting everything out of them. So yeah, let’s fix that today.
🔗 Link to the code on GitHub
1. enumerate() – Not Just for Counting from Zero
When looping through collections, enumerate()
is your go-to function for tracking position.
def generate_leaderboard(players):
leaderboard = []
for i, player in enumerate(players):
# Manually adding 1 because leaderboards start at 1, not 0
leaderboard.append(f"i+1. player['name']: player['score'] pts")
return leaderboard
top_players = [
'name': 'MasterBlaster', 'score': 157,
'name': 'QuickShot', 'score': 145,
'name': 'StealthNinja', 'score': 132
]
print('\n'.join(generate_leaderboard(top_players)))
Why it’s problematic: Adding to the index inside the loop creates unnecessary visual noise, especially when you revisit the code months later. It’s also less self-documenting. A new team member would need to trace through the code to understand why you’re adding 1.
def generate_leaderboard(players):
# The start parameter makes our intention crystal clear
leaderboard = []
for rank, player in enumerate(players, start=1):
leaderboard.append(f"rank. player['name']: player['score'] pts")
return leaderboard
print('\n'.join(generate_leaderboard(top_players)))
Why it’s better: The start parameter explicitly tells us that we want to count from 1, making it clearer. This approach is also more maintainable. If you ever need to change the starting number (perhaps for pagination), you only need to change one value.
Both these approaches will give you the following output:
1. MasterBlaster: 157 pts
2. QuickShot: 145 pts
3. StealthNinja: 132 pts
2. sorted() – Underutilizing Multi-Key Sorting
When it comes to organizing complex data, Python’s sorted()
function can save you from writing convoluted sorting logic.
def organize_inventory(products):
# First sort by quantity (least to most)
by_quantity = sorted(products, key=lambda x: x['quantity'])
# Then sort by category
by_category = sorted(by_quantity, key=lambda x: x['category'])
# Finally sort by priority
final_sorted = sorted(by_category, key=lambda x: x['priority'])
return final_sorted
inventory = [
'name': 'Laptop', 'category': 'Electronics', 'quantity': 5, 'priority': 1,
'name': 'Headphones', 'category': 'Electronics', 'quantity': 8, 'priority': 2,
'name': 'Notebook', 'category': 'Office', 'quantity': 15, 'priority': 3,
'name': 'Pen', 'category': 'Office', 'quantity': 50, 'priority': 3
]
Why it’s problematic: This approach is not only inefficient but actually incorrect. Each sort destroys the previous ordering except for items with identical values. To get the right order, you’d need to sort from least to most important key — the reverse of what most people intuitively do — which is confusing and error-prone.
def organize_inventory(products):
return sorted(products, key=lambda x: (
x['priority'], # Sort by priority first
x['category'], # Then by category
x['quantity'] # Then by quantity
))
Why it’s better: The tuple approach handles all sort keys in a single pass, making it both more efficient and more maintainable. It clearly communicates the sorting priority through the order of the tuple elements. If you need to change sort directions, it’s as simple as using -x['quantity']
for numbers or implementing a custom reversal for strings.
['name': 'Laptop', 'category': 'Electronics', 'quantity': 5, 'priority': 1, 'name': 'Headphones', 'category': 'Electronics', 'quantity': 8, 'priority': 2, 'name': 'Notebook', 'category': 'Office', 'quantity': 15, 'priority': 3, 'name': 'Pen', 'category': 'Office', 'quantity': 50, 'priority': 3]
3. map() – Overusing Lists and Lambdas
The map()
function is a powerful tool for transforming data, but many Python developers don’t leverage its full potential or understand when to use it over alternatives.
def process_customer_data(customers):
# Extract all customer email domains
emails = [customer['email'] for customer in customers]
domains = list(map(lambda email: email.split('@')[1], emails))
# Count domains
domain_counts =
for domain in domains:
if domain in domain_counts:
domain_counts[domain] += 1
else:
domain_counts[domain] = 1
return domain_counts
customers = [
'name': 'Alice', 'email': 'alice@example.com',
'name': 'Bob', 'email': 'bob@gmail.com',
'name': 'Charlie', 'email': 'charlie@example.com'
]
Why it’s problematic: This code uses map()
with a lambda function when a more elegant solution exists. It also converts the result to a list when the values are only used once in a loop, wasting memory.
def process_customer_data(customers):
# Extract domain directly with a function reference
domains = map(lambda c: c['email'].split('@')[1], customers)
# Count domains using a Counter
from collections import Counter
return Counter(domains) # No need to convert to list first
Why it’s better: This version passes the domain extraction function directly to map()
and maintains the lazy iterator nature of the result. The Counter class is perfect for this counting operation, making the code more concise and efficient. For simple transformations, consider list comprehensions, but map()
works great when applying existing functions or when working with multiple iterables in parallel.
process_customer_data(customers)
You get:
Counter('example.com': 2, 'gmail.com': 1)
4. all() and any() – Creating Unnecessary Lists
The boolean functions any()
and all()
are useful but frequently misused in ways that negate their performance benefits.
def validate_user_submissions(submissions):
# Check if any submission has been flagged
has_flagged = any([submission['flagged'] for submission in submissions])
# Check if all submissions are complete
all_complete = all([submission['complete'] for submission in submissions])
# Determine status based on checks
if has_flagged:
return "REVIEW_NEEDED"
elif all_complete:
return "COMPLETE"
else:
return "IN_PROGRESS"
user_submissions = [
'id': 1, 'complete': True, 'flagged': False,
'id': 2, 'complete': True, 'flagged': True,
'id': 3, 'complete': False, 'flagged': False
]
Why it’s problematic: Creating a full list in memory before passing it to all()
or any()
defeats the short-circuiting behavior that makes these functions efficient. With large datasets, this can waste significant memory and processing time.
def validate_user_submissions(submissions):
# Generator expressions don't create full lists
has_flagged = any(sub['flagged'] for sub in submissions)
# This stops checking as soon as it finds an incomplete submission
all_complete = all(sub['complete'] for sub in submissions)
if has_flagged:
return "REVIEW_NEEDED"
elif all_complete:
return "COMPLETE"
else:
return "IN_PROGRESS"
Why it’s better: Generator expressions allow these functions to stop evaluation as soon as the result is determined.
- For
any()
, this means returning True immediately upon finding the first truthy value. - For
all()
, it returns False as soon as it encounters a falsy value.
This can improve performance when working with large datasets or when the condition might be satisfied early in the sequence.
5. zip() – Ignoring Truncation Behavior
The zip()
function elegantly pairs elements from multiple iterables, but its default behavior can lead to subtle bugs if you’re not careful.
def assign_mentors(students, mentors):
# Pair students with mentors
assignments = []
for student, mentor in zip(students, mentors):
assignments.append(f"student will be mentored by mentor")
return assignments
students = ['Alice', 'Bob', 'Charlie', 'Diana']
mentors = ['Dr. Smith', 'Prof. Jones'] # Only two mentors available
# This silently drops Charlie and Diana :(
print('\n'.join(assign_mentors(students, mentors)))
This outputs:
Alice will be mentored by Dr. Smith
Bob will be mentored by Prof. Jones
Why it’s problematic: Standard zip()
silently truncates to the shortest input sequence, which means students without mentors simply vanish from the output without any error or warning. This data loss can lead to subtle bugs that are hard to track down.
from itertools import zip_longest
def assign_mentors(students, mentors):
# Use zip_longest
assignments = []
for student, mentor in zip_longest(students, mentors, fillvalue="MENTOR NEEDED"):
assignments.append(f"student will be mentored by mentor")
return assignments
print('\n'.join(assign_mentors(students, mentors)))
Why it’s better: zip_longest()
ensures that no data is silently dropped. By specifying a fillvalue, you make the absence of matching elements explicit and provide a clear indicator that action needs to be taken. This approach is more robust and prevents the kind of subtle data loss that can occur with regular zip()
.
This second approach gives the correct output:
Alice will be mentored by Dr. Smith
Bob will be mentored by Prof. Jones
Charlie will be mentored by MENTOR NEEDED
Diana will be mentored by MENTOR NEEDED
6. dict.get() – Not Using Default Values Effectively
It’s time to look at a method that’s often used suboptimally. Dictionary handling in Python can be elegant or clunky depending on how you use the get()
method. Instead of doing something like this:
def process_user_preferences(user_profile):
# Verbose handling of optional values
if 'theme' in user_profile:
theme = user_profile['theme']
else:
theme="default"
# Or the equally problematic:
notifications = user_profile.get('notifications')
if notifications is None:
notifications = 'email': True, 'sms': False
# Process preferences
return
'theme_css': f"themes/theme.css",
'notification_settings': notifications
user =
'name': 'Alex',
'email': 'alex@example.com'
# Missing theme and notifications
Why it’s problematic: The first approach is unnecessarily verbose, while the second still requires a conditional check after using get()
. Both make the code harder to read and introduce more opportunities for errors.
def process_user_preferences(user_profile):
# Clean, one-line default values
theme = user_profile.get('theme', 'default')
notifications = user_profile.get('notifications', 'email': True, 'sms': False)
return
'theme_css': f"themes/theme.css",
'notification_settings': notifications
Why it’s better: Using the second parameter of dict.get()
eliminates verbose conditionals. This approach reduces branching, improves readability, and prevents the common error of accessing a non-existent key. It’s particularly valuable when processing API responses, user inputs, or configuration data where missing values are common.
7. functools.lru_cache() – Misconfiguring the Cache
We’ll wrap up by going over a commonly misused decorator: lru_cache
from the functools module. Memoization is a powerful optimization technique, but many developers don’t use Python’s built-in caching decorator well.
from functools import lru_cache
@lru_cache
def get_user_permissions(user_id):
"""Fetch user permissions from database"""
print(f"DB Query for user user_id") # Side effect to show cache misses
# Simulate expensive database query
import time
time.sleep(0.1)
return ['read', 'write'] if user_id % 2 == 0 else ['read']
def check_permission(user_id, permission):
permissions = get_user_permissions(user_id)
return permission in permissions
Why it’s problematic: Using lru_cache
without parameters means accepting the default cache size of 128 entries. For a function that might be called with thousands of different user IDs, this could lead to frequent cache evictions and unnecessary database queries.
@lru_cache(maxsize=1024, typed=True)
def get_user_permissions(user_id):
"""Fetch user permissions from database"""
print(f"DB Query for user user_id")
# Simulate expensive database query
import time
time.sleep(0.1)
return ('read', 'write') if user_id % 2 == 0 else ('read',) # Tuple is immutable
Why it’s better: This version explicitly sets a larger cache size appropriate for the expected number of unique users. The typed=True parameter makes the cache differentiate between different types of arguments (like int vs str user IDs), preventing potential bugs. Using immutable return values (tuples instead of lists) is also good practice with cached functions to prevent unexpected side effects.
You can now use it like so:
def check_permission(user_id, permission):
permissions = get_user_permissions(user_id)
return permission in permissions
Wrapping Up
These subtle differences in how you use Python’s built-in functions might seem minor at first glance, but they add up to create code that’s more readable, efficient, and maintainable. The best Python developers don’t just make their code work. they leverage the language’s full capabilities to write elegant solutions.
Next time you reach for one of these functions, take a moment to consider whether you’re using it optimally. Small improvements in your everyday coding patterns compound over time, leading to significantly better codebases and fewer headaches down the road.
What function have you been using for years only to discover you weren’t using it to its full potential? We’ve all had that moment—share yours in the comments!
Bala Priya C is a developer and technical writer from India. She likes working at the intersection of math, programming, data science, and content creation. Her areas of interest and expertise include DevOps, data science, and natural language processing. She enjoys reading, writing, coding, and coffee! Currently, she’s working on learning and sharing her knowledge with the developer community by authoring tutorials, how-to guides, opinion pieces, and more. Bala also creates engaging resource overviews and coding tutorials.