From My Notebook: 100 Python Tricks to Get a Job at Google (or Any Big Company)
Unlocking 100 Python secrets from my notes, complete with examples. Learn the typical and the top-tier approaches. Ready for Big Tech?

Embarking on a quest to uncover 100 gems of Python wisdom? Welcome aboard! I’ve curated this collection straight from my personal notebook, where I’ve jotted down every intriguing Python tidbit I’ve chanced upon. While I’ve endeavored to arrange them in a sequence that feels logical, remember that their order reflects my own journey of discovery. If you’re keen on leveling up your Python game, you’re in for a treat. By the time you finish this list, you’ll have insights that could turn some heads in the tech industry. I invite you to accompany me on this enlightening trek. And if any of these tricks add a spark to your Python prowess, your clap on this article and a follow would mean the world to me. Let’s get started on this pythonic adventure, because we’ll have a lot of work to do!

1. The Pythonic Way
Normal: Hey, remember those days when we’d use those long-winded for-loops just to go over lists? Kind of like manually rolling down car windows, right?
new_list = []
for item in old_list:
new_list.append(item * 2)Pro Trick: Well, Python introduced us to this sleek method called list comprehensions. It’s like having automatic window controls in modern cars. Quick, elegant, and oh-so-handy!
new_list = [item * 2 for item in old_list]Trust me, once you start using this, you’ll wonder how you ever did without it!
2. Unpacking Basics
Normal: You know, back in the day, if we wanted to pull items out of a list, we’d do it one at a time. Kind of like picking out candies from a jar one-by-one.
fruits = ["apple", "banana", "cherry"]
apple = fruits[0]
banana = fruits[1]
cherry = fruits[2]Pro Trick: But Python has this nifty feature called unpacking. It’s like tipping that candy jar and getting all your favorite candies in one go!
apple, banana, cherry = fruits
Such a time-saver, right? It’s always nice when you can grab all the goodness in one swift move.
3. Advanced pytest Techniques for Testing
Normal: In the early days, writing unit tests felt a bit like manually winding up a music box, using Python’s standard unittest. It got the job done, but there was a lot of repetitive cranking involved.
import unittest
class TestMath(unittest.TestCase):
def test_add(self):
self.assertEqual(1 + 2, 3)
if __name__ == '__main__':
unittest.main()Pro Trick: But then I stumbled upon pytest, and it felt like upgrading to one of those fancy self-playing pianos! Not only does it play the tune, but it adds a little jazz with its advanced features and fixtures.
import pytest
def add(a, b):
return a + b
def test_add():
assert add(1, 2) == 3What I love about pytest is the simplicity. No need to set up classes; just write your function and the test. Plus, its plugins and concise syntax feel like the cherry on top of a perfect sundae!
4. Memory Profiling with memory-profiler
Normal: I remember the times when eyeballing code was the main way to judge if it was memory-efficient. We used to run our programs, hope for the best, and if something felt off, we’d just keep our fingers crossed and make educated guesses on where the leak might be.
def large_data_operation():
big_list = [i for i in range(10000000)]
# Some complex operations on big_list
return sum(big_list)
result = large_data_operation()Pro Trick: But then, tools like memory-profiler came along and it felt as if we'd been given x-ray glasses for our code. With it, you can see line-by-line memory usage, making memory leaks and inefficient allocations stand out like a sore thumb.
from memory_profiler import profile
@profile
def large_data_operation():
big_list = [i for i in range(10000000)]
# Some complex operations on big_list
return sum(big_list)
result = large_data_operation()Now, with such tools, it’s almost like having a friendly guide showing you around, pointing out spots where you could improve. No more flying blind; it’s all about informed decisions for memory efficiency. And that’s a game-changer.
5. Enumerate for Index and Value
Normal: Back in the day, if we wanted to keep tabs on an item’s position in a list while iterating, we’d typically set up a separate counter, incrementing it step by step. A tad manual, but it did the job.
fruits = ["apple", "banana", "cherry"]
index = 0
for fruit in fruits:
print(f"Index {index}: {fruit}")
index += 1Pro Trick: But then I learned about the enumerate() function, and it was a breath of fresh air! It's like having a tiny helper that hands you both the index and the value as you loop through items, without the need for an external counter.
fruits = ["apple", "banana", "cherry"]
for index, fruit in enumerate(fruits):
print(f"Index {index}: {fruit}")It’s these little improvements, these neat shortcuts, that make the coding process smoother and more intuitive. A simple change, but such a pleasant one!
6. Using dataclasses
Normal: In my earlier coding adventures, if I wanted to represent a simple data structure, I would use a classic class definition. But often, I found myself manually writing out a bunch of methods, like __init__ or __repr__, just to manage and visualize the data. Imagine doing this for a book:
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
def __repr__(self):
return f"Book({self.title!r}, {self.author!r}, {self.year!r})"It gets the job done, but feels a bit repetitive, especially for larger classes.
Pro Trick: And then I discovered dataclasses! With this Python gem, the boilerplate reduces drastically. The same class, when transformed with dataclasses, looks so clean:
from dataclasses import dataclass
@dataclass
class Book:
title: str
author: str
year: intThat’s it! The initializer and representation methods are auto-magically added for you. It’s like having a little coding elf doing the mundane tasks so you can focus on the fun stuff. Makes you wonder about all those hours spent on manual method definitions, doesn’t it?
7. Advanced Unpacking with PEP 448
Normal: In the old days, I’d use basic unpacking to grab elements from a list or tuple. For example, when handling coordinates:
coords = (10, 20, 30) x, y, z = coords
This works perfectly for many scenarios. But what if you have a lengthy list and only need the first few items and the last one?
Pro Trick: Enter PEP 448! It introduced some snazzy extended unpacking techniques. With it, you can now perform unpacking gymnastics like:
numbers = [1, 2, 3, 4, 5, 6]
first, *middle, last = numbers
print(first) # 1
print(middle) # [2, 3, 4, 5]
print(last) # 68. Generators Inside-Out with yield and yield from
Normal: I remember the time when I’d loop through a large dataset in one go, consuming a lot of memory. Classic iterations over collections are kind of like binging your favorite TV show: all at once, but then you’re left feeling a bit overwhelmed.
def read_data(dataset):
result = []
for data in dataset:
result.append(data)
return resultPro Trick: Instead of binging, what if you could enjoy your show (or, in this case, your data) piece by piece, savoring each moment? That’s the magic generators bring. Generators allow you to process data lazily, fetching items one at a time, reducing memory usage. They’re your secret weapon for handling massive datasets without the memory drain. You achieve this with the yield keyword:
def read_data_lazy(dataset):
for data in dataset:
yield dataNow, suppose you have nested datasets, and you want to yield from them in a sequence. Here’s where yield from shines. It simplifies the code and makes it more readable.
def nested_data_gen(data_sets):
for dataset in data_sets:
yield from datasetIncorporating generators is like having a remote with a pause button for your data streams, giving you full control and efficiency. Embrace them, and you’ll start seeing performance leaps in your projects!
9. Using pathlib for Path Manipulations
Normal: Back in the day, when working with file paths, it felt a lot like tying shoelaces with gardening gloves on. Clumsy and intricate. Many of us relied on a mix of string manipulations and the os module to get things done.
import os
current_dir = os.getcwd()
file_path = os.path.join(current_dir, 'sample.txt')
file_name = os.path.basename(file_path)Pro Trick: Enter pathlib, which I like to think of as the modern knife for path manipulations. It turns those clumsy string operations into intuitive, object-oriented operations, making your code cleaner and more readable.
from pathlib import Path
current_dir = Path.cwd()
file_path = current_dir / 'sample.txt'
file_name = file_path.nameWith pathlib, it feels like the paths come alive, and operations on them become second nature. Whether you're checking for a file's existence, extracting file extensions, or creating new directories, pathlib wraps all these in a neat package, simplifying your code and life. So, next time you're dealing with file paths, give pathlib a whirl. It might just become your new best coding buddy!
10. Itertools’ Lesser-Known Gems
Normal: Let’s say you’ve set out to work on some iteration patterns. The good ol’ fashioned way, piecing together custom logic like a jigsaw puzzle. For instance, generating pairs of items from a list.
my_list = [1, 2, 3, 4]
pairs = [(my_list[i], my_list[j]) for i in range(len(my_list)) for j in range(i+1, len(my_list))]Pro Trick: Now, imagine a treasure chest. The itertools module is exactly that, but for iteration! Some of its functions are like those secret compartments in the chest, hidden and waiting to be discovered. Take the combinations function, for example. It elegantly achieves the same as the above, but without the rigmarole.
import itertools
pairs = list(itertools.combinations(my_list, 2))Diving into itertools, you'd find many such gems that can transform the way you think about iterations. From chaining multiple lists to generating infinite patterns, itertools truly lives up to its name. So, why reinvent the wheel when you've got a sparkling toolkit right under your nose? The next time you find yourself wrestling with complex loops, remember: there might just be an itertools gem waiting to make your day brighter!
11. The inspect Module Insights
Normal: So, picture this. You’re working with a chunk of Python code, and you’re trying to figure out how a particular function operates. The usual approach? You’d likely sprinkle in some print statements, or if you’re fortunate, stumble upon some well-crafted docstrings that offer a semblance of guidance.
def mystery_function(a, b):
"""Does something mysterious!"""
return a + b
# Trying to understand the function
print(mystery_function.__doc__)Pro Trick: But what if I told you there’s a magnifying glass at your disposal, allowing you to explore the inner workings of Python objects? Enter the inspect module. This little toolkit can reveal parameters of functions, pull out the source code, and even tell you if an object is a generator.
import inspect
# Get the signature of the function
print(inspect.signature(mystery_function))
# Get the source code
print(inspect.getsource(mystery_function))It’s akin to having a backstage pass to a concert! You’re no longer limited to just enjoying the show; you get to see all the behind-the-scenes magic. The inspect module offers you that very opportunity with Python. So, the next time you're lost in a maze of Python objects, reach for your backstage pass and unravel the mysteries that lie beneath!
12. Customizing Enumerations with Enum Class
Normal: Imagine you’ve been tasked with categorizing different species of apples in your coding project. A direct approach? You might use a tuple or a list with string values to represent these species.
APPLE_SPECIES = ('FUJI', 'HONEYCRISP', 'GALA')
print(APPLE_SPECIES[0]) # Outputs: FUJINow, this works just fine. But as the project scales and complexity burgeons, you might start scratching your head, thinking, “Was HONEYCRISP index 1 or 2?"
Pro Trick: Here’s where the Enum class dances into the scene! By employing the Enum class, you can both capture the essence of apple species and impart a touch of readability and robustness to your code.
from enum import Enum
class AppleSpecies(Enum):
FUJI = 1
HONEYCRISP = 2
GALA = 3
print(AppleSpecies.FUJI.name) # Outputs: FUJI
print(AppleSpecies.FUJI.value) # Outputs: 1There’s an elegance to the Enum approach. It’s not just about indexing anymore. Each apple species gets an identity — a name and a unique value. This not only helps in avoiding accidental overlaps but also makes the code self-descriptive. It’s like painting a picture with a palette full of vibrant colors instead of just sketching in grayscale. The Enum class helps you craft code that speaks for itself, vividly and with purpose. So, the next time you're about to enumerate something in Python, think colorful!
13. Dynamic Function Arguments
Normal: Ever been to a local diner where the menu is just a list of fixed combo meals? Sure, they’re quick and straightforward, but there’s little room for customization. Similarly, in the programming realm, you often start with defining functions having a set number of arguments.
def calculate_area(length, width):
return length * width
# When we call it:
area = calculate_area(5, 10)
print(area) # Outputs: 50Now, that’s straightforward. But what if your geometric whims take a turn, and now you want to calculate the volume, or perhaps, factor in the height or depth?
Pro Trick: Welcome to the versatile world of *args and **kwargs! Think of them as the “Build Your Own Meal” option in Python functions. Whether you have two ingredients or twenty, you’re covered.
def calculate_dimensions(*args):
result = 1
for dimension in args:
result *= dimension
return result
# Want area? Pass in 2 values. Want volume? 3 values will do the trick!
print(calculate_dimensions(5, 10)) # Outputs: 50
print(calculate_dimensions(5, 10, 2)) # Outputs: 100And that’s not all. With **kwargs, you can even name these dynamic ingredients:
def print_data(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_data(Name='John', Age=25, Country='USA')Just like customizing your sandwich with all your favorite fillings, *args and **kwargs let you tailor your functions to your heart’s content. So the next time you’re penning down a function and aren’t quite sure of all the ingredients you might need — remember, Python’s got a dynamic duo ready to serve!
14. Concurrency with concurrent.futures
Normal: Imagine you’ve got a few tasks on your to-do list. One option? Tackle each task one by one. In the coding realm, that’s a bit like using basic threading or multiprocessing. It gets the job done, but perhaps not in the most efficient manner.
import threading
def print_numbers():
for i in range(1, 6):
print(i)
def print_letters():
for letter in 'abcde':
print(letter)
# Start two threads
t1 = threading.Thread(target=print_numbers)
t2 = threading.Thread(target=print_letters)
t1.start()
t2.start()
t1.join()
t2.join()In this setup, we’re essentially managing our threads manually. And sure, it’s a step up from a linear approach. But can we make it more graceful? Enter our pro tip.
Pro Trick: Ever considered a personal assistant who can manage multiple tasks for you simultaneously? That’s essentially what concurrent.futures is—a high-level interface for asynchronously executing callables. With it, you can elegantly pool threads or processes without getting into the nitty-gritty.
import concurrent.futures
def compute_square(n):
return n * n
# Using ThreadPoolExecutor to run tasks in parallel
numbers = [1, 2, 3, 4, 5]
with concurrent.futures.ThreadPoolExecutor() as executor:
results = list(executor.map(compute_square, numbers))
print(results) # Outputs: [1, 4, 9, 16, 25]With concurrent.futures, managing concurrency feels almost like having a swanky new tool that handles the busy work. It’s the kind of sophistication that makes you think, "Ah, why didn't I use this earlier?" So next time you're juggling tasks, remember: Python offers a smoother way to keep all those balls in the air!
15. The dis Module for Bytecode Analysis
Normal: Picture this: You’re trying to figure out a puzzle, but you’re only looking at the surface pieces. That’s a bit like debugging with just print statements or basic tools. They can be helpful, sure, but sometimes you really need to dive deeper to truly understand what’s going on.
def add(a, b):
result = a + b
print(result)
# Debugging using print
add(3, 4) # Outputs: 7In this method, we’re using our trusty print to see the outcome. It's a tried and true method, but it doesn't give you the full story of what's happening under the hood.
Pro Trick: Now, imagine having X-ray vision that lets you see not just the surface, but all the intricate inner workings. That’s where the dis module steps in, allowing you to analyze your Python code at the bytecode level. It’s like understanding the machinery behind the magic!
import dis
def add(a, b):
result = a + b
return result
dis.dis(add)When you run this, you’ll see a detailed breakdown of the bytecode operations behind our simple function. This might seem like overkill for our humble add function, but for more complex code? It’s a revelation. The dis module offers a deeper level of insight that can be a game changer when you're trying to get to the root of intricate issues.
Remember, sometimes the best way to understand something fully is to see its innermost layers. And for Python, the dis module is your X-ray tool of choice!
16. Advanced Decorators and Context Managers
Normal: Let’s imagine you’ve just started learning to cook. At first, you might simply follow recipes, measuring ingredients, and hoping things turn out well. This is similar to using basic function wrappers in Python. Say you want to measure the time it takes for a function to run:
import time
def time_it(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.2f} seconds.")
return result
return wrapper
@time_it
def slow_function():
time.sleep(2)
slow_function() # Outputs: slow_function took 2.00 seconds.That’s a neat trick, right? And for managing resources like files, you might open, operate, and close them manually. It gets the job done but isn’t always the most efficient.
Pro Trick: Now, as you evolve in your cooking journey, you start inventing dishes, using tools more efficiently, and cooking becomes an art. Similarly, in Python, as you progress, you might start creating advanced decorators, making your functions even more powerful and efficient. Plus, with context managers, managing resources becomes a breeze:
from contextlib import contextmanager
@contextmanager
def timed_operation():
start = time.time()
yield
end = time.time()
print(f"The operation took {end - start:.2f} seconds.")
with timed_operation():
slow_function() # Outputs: The operation took 2.00 seconds.The beauty of this trick is that you can use the timed_operation context anywhere in your code without having to modify the functions. It’s a touch of elegance, saving you both time and effort.
Think of it like mastering a tricky dish. Sure, it takes a bit more effort to learn, but once you have it down, it’s a showstopper that impresses everyone at the table!
17. The Secrets of Metaclasses
Normal: Picture this: You’ve always played with the same set of LEGO blocks, stacking them together to create various structures. This is akin to using Python’s standard class-based structures:
class SimpleClass:
def __init__(self, name):
self.name = nameIt’s straightforward, just like stacking those familiar LEGO blocks. You know the drill, and you can construct something decent.
Pro Trick: But what if I told you there’s a magical LEGO set out there that lets you define how the blocks themselves are created? It’s a game-changer, right? Enter the world of metaclasses in Python. Metaclasses allow you to customize class creation behaviors:
class UppercaseAttributesMeta(type):
def __new__(cls, name, bases, class_dict):
uppercase_attributes = {
key.upper(): val for key, val in class_dict.items()
}
return super(UppercaseAttributesMeta, cls).__new__(
cls, name, bases, uppercase_attributes
)
class CustomClass(metaclass=UppercaseAttributesMeta):
bar = "Hello"
print(hasattr(CustomClass, 'bar')) # Outputs: False
print(hasattr(CustomClass, 'BAR')) # Outputs: TrueWith this pro trick, attributes in the CustomClass are automatically turned uppercase! Metaclasses can be powerful, allowing you to add a layer of logic before your classes are even created.
It’s like discovering a secret compartment in your LEGO set that opens up a whole new realm of possibilities. Sure, there’s a steeper learning curve, but the creative freedom you get is exhilarating. Dive in, and don’t be afraid to explore these hidden depths!
18. Memory Views and Buffer Protocols
Normal: Imagine we’re sitting at a dinner table with a large pizza in front of us. Now, each time we want a slice, we call over the waiter and have him bring a brand-new pizza, cut it, and serve us just one piece, leaving the rest untouched. That’s quite inefficient, isn’t it? Similarly, when we work directly with binary data in memory, it feels just like that. We often copy and recreate data even when it’s not necessary:
data = bytearray(b"Hello, World!")
copy = data[7:12]
print(copy) # Outputs: b'World'We’ve sliced a portion of our “pizza” (or, in this case, our data), but we’ve also unnecessarily created a whole new pizza.
Pro Trick: Let’s rethink our dinner strategy. Instead of the entire pizza ordeal, what if we just had a transparent overlay that let us focus on the slice we want, without actually making a new pizza? That’s exactly the principle behind memory views:
data = bytearray(b"Hello, World!")
view = memoryview(data)[7:12]
print(view.tobytes()) # Outputs: b'World'By using a memory view, we access the slice we want without copying the underlying data, much like our pizza overlay. It’s an efficient and safe way to manipulate data, ensuring we aren’t wastefully creating new copies.
Just as the right tools make dinner a breeze, memory views can be a game-changer for handling binary data. Think of them as your secret ingredient, making your code more robust and resourceful. Enjoy every byte, just like you’d savor each slice of pizza!
19. Conditional Assignment
Normal: Picture this: you’re browsing through a menu at a cozy restaurant, deciding between two desserts. But instead of choosing one right away, you call the waiter over, ask him about the first option, then wait. Then you ask about the second, and wait. Then you finally make your choice. A tad roundabout, isn’t it? When we use a series of if-else statements for variable assignments, it’s a bit like this drawn-out dessert decision:
weather = "sunny"
if weather == "sunny":
mood = "happy"
else:
mood = "gloomy"
print(mood) # Outputs: 'happy'We’ve essentially asked the waiter twice before making a decision.
Pro Trick: Now, imagine quickly glancing at the menu and making a swift choice between those two desserts. That’s the elegance the ternary operator brings:
weather = "sunny"
mood = "happy" if weather == "sunny" else "gloomy"
print(mood) # Outputs: 'happy'With the ternary operator, our decision (or in this case, our assignment) becomes straightforward and quick, letting us enjoy our dessert (or our output) much sooner!
So, next time you’re faced with a simple choice in your code, think of your favorite desserts and remember the power of conditional assignment. It might just sweeten your coding experience!
20. Chaining Comparisons
Normal: Imagine you’re in a bookstore. You’ve got a gift card with a specific budget, and you want to make sure the book you buy is neither too pricey nor too cheap. You’d probably check if the price is greater than your minimum budget and then separately ensure it’s less than your maximum budget. That’s a lot like using multiple conditional statements in Python:
price = 15
if price > 10 and price < 20:
print("Within budget!")This method works, but it feels like checking each book’s price twice before putting it in your cart.
Pro Trick: What if, instead of juggling two price checks, you could glance at the book’s price tag and immediately know if it falls within your budget? That’s the magic of chaining comparisons:
if 10 < price < 20:
print("Within budget!")With this chained comparison, the logic becomes crisp and concise. It’s like having a sixth sense about whether that book is just right for your gift card.
When you think about it, programming often mirrors our daily decision-making processes. And just as in real life, in coding, sometimes the shortest path offers the clearest journey. So, the next time you find yourself weighing options in your code, give chaining comparisons a try! It might make your decision-making a tad bit clearer.
21. Expanding with * in Function Calls
Normal: You know those times when you’re setting up a game night and you’re handing out cards to each player one by one? That’s a bit like how we usually pass arguments to functions:
def game_cards(player1, player2, player3):
print(f"{player1}, {player2}, and {player3} are ready to play!")
game_cards('Alice', 'Bob', 'Charlie')It’s pretty straightforward. Each player gets a card, and we’re all set to kick off the game.
Pro Trick: But what if you had a machine that could distribute cards to all players in one go? Wouldn’t that speed things up? In Python, this quick distribution is akin to using the * operator:
players = ['Alice', 'Bob', 'Charlie']
game_cards(*players)Voilà! All the players have their cards, and you’ve saved some valuable dealing time. This trick is particularly handy when you’re unsure of the number of players beforehand.
Think of the * operator as that helpful friend who’s always ready to speed up the game setup so everyone can dive right into the fun. And in coding, just as in game night, getting everyone on board smoothly makes the whole experience so much better. So, next time you see a list of items waiting to be passed into a function, remember this neat trick and spread the fun all at once!
22. Exception Handling: Beyond the Basics
Normal: Imagine you’re setting up a board game with dice. Every time you roll the dice, there’s a chance it could land off the table, get lost under the couch, or worse, be swallowed by your overly curious pet. In the coding world, these unexpected mishaps are what we’d call exceptions. The traditional way to handle this is by having a simple rule:
try:
dice_result = roll_dice()
except DiceOffTable:
print("Oops! Roll again.")It’s a good practice, no doubt. It ensures that if the dice rolls off the table, everyone has a good laugh, retrieves it, and then continues the game.
Pro Trick: But what if you could make your game night even smoother? Like having a rule that says, if the dice roll is successful, applaud the roller. And regardless of the outcome, always remind everyone to be careful with their roll:
try:
dice_result = roll_dice()
except DiceOffTable:
print("Oops! Roll again.")
else:
print("Great roll! Let's see what you got.")
finally:
print("Remember, gentle rolls, everyone!")By adding the else clause, you've recognized and cheered on a successful dice roll. And the finally clause? That's your friendly reminder, ensuring that whether the roll was a mishap or a success, everyone remains cautious for the next one.
This approach to exception handling is like having a little board game moderator sitting with you, making the experience delightful for everyone, ensuring laughs on mishaps, cheers on successes, and gentle nudges of caution. Next time you’re writing code, think of this extended try-except block as that moderator, enhancing the game of coding for you!
23. Merging Dictionaries Efficiently
Normal: Think of dictionaries as recipe books. If you have two separate recipe books and want to combine them into one, the usual approach might be to manually copy each recipe from one book and paste it into the other. In Python, that’s like:
dict1 = {"apple pie": "delicious", "brownies": "yummy"}
dict2 = {"chocolate cake": "heavenly", "tiramisu": "exquisite"}
merged_dict = dict1.copy()
for key, value in dict2.items():
merged_dict[key] = value
# or you can update dict1 with dict2 valuesIt’s a bit tedious, but it gets the job done. Your recipes (or dictionary entries) are safely combined.
Pro Trick: But what if there was a magical photocopier that could instantly copy and combine the contents of both recipe books into one? In the Python world, that’s dictionary unpacking:
merged_dict = {**dict1, **dict2}It’s like that delightful moment when you have all your favorite recipes in one place with just a snap of your fingers!
Bonus: Since you brought up the latest Python tricks, in Python 3.9, merging dictionaries became even more elegant. Now, it’s like having a magical binder that automatically combines your recipe books:
merged_dict = dict1 | dict2Pretty neat, right? Now, whether you’re baking or coding, combining the best parts can be done in a jiffy. Happy baking and happy coding!
24. Dive Deep with Deepcopy
Normal: Picture this: You’ve just crafted the most intricate paper boat. It’s not just any boat — it’s your boat. Your friend asks for one too, so you trace around your original on a fresh piece of paper. But later, you notice when they fold their paper, your original boat gets crumpled as well! In Python, this often happens when you think you’ve made a fresh copy of a list or dictionary, but in reality, they’re still connected:
original_boat = [['mast'], ['deck'], ['sail']]
traced_boat = original_boat
traced_boat[0][0] = "anchor"
print(original_boat[0]) # Surprise! It's now ['anchor']Both the original and the traced boats get affected because they reference the same memory spot.
Pro Trick: What if you could use a special craft tool that cuts and recreates every tiny detail, ensuring your original boat remains untouched? Enter the deepcopy:
from copy import deepcopy
original_boat = [['mast'], ['deck'], ['sail']]
crafted_boat = deepcopy(original_boat)
crafted_boat[0][0] = "anchor"
print(original_boat[0]) # Voila! It's still ['mast']With deepcopy, changes to the crafted boat don’t ripple back to the original. It's genuinely a whole new boat, free to sail on its own voyage without dragging the other one with it.
Remember, when things get intricate and connected, sometimes you need to dive a little deeper to keep them truly separate. Happy crafting and coding!
25. Profiling with py-spy
Normal: Imagine you’ve built a captivating marble maze. Your marble seems to be dawdling at certain spots, but you can’t quite figure out where the slowdowns are happening just by looking at it. You try timing different parts with a stopwatch, but it’s not offering the whole picture. In the Python world, we often turn to in-built profilers to analyze our code’s performance:
import cProfile
def slow_function():
sum(range(1000000))
cProfile.run('slow_function()')This gives us a snapshot of where time is spent, but it can be a bit clunky and sometimes we want a more vivid picture.
Pro Trick: Enter the marvel that is py-spy. Think of it as setting up a slow-motion camera right above your marble maze. It captures everything seamlessly, allowing you to replay and observe every twist and turn in detail:
$ py-spy top -- python your_script.pyWithout even touching your Python code, py-spy offers you a real-time performance visualization. It's like watching an aerial view of your marble, spotting exactly where it slows down or gets stuck, allowing you to perfect those tricky corners.
With tools like py-spy, you're not just blindly navigating the maze. You get a bird's-eye view, and with that knowledge, the path to optimization becomes so much clearer. Keep rolling and refining!
26. Simplified String Manipulation
Normal: Picture this: You’re trying to assemble a jigsaw puzzle. You’ve got all these pieces laid out, and you’re putting them together one by one. Sometimes it works, sometimes it’s clumsy, and you think there must be a better way. When you’re dealing with strings in Python, it might feel similar. Often, you might find yourself concatenating them like this:
sentence = "Hello, " + "world!" + " How " + "are " + "you?"It gets the job done, but especially for larger texts, it’s like piecing together that jigsaw puzzle one piece at a time.
Pro Trick: Now, what if you had a magic frame where you could just lay out your jigsaw pieces and they’d automatically snap together, forming the complete picture? That’s what the join() method does for strings:
words = ["Hello, ", "world!", " How ", "are ", "you?"]
sentence = ''.join(words)With join(), string concatenation becomes a breeze. It's like having that magic frame for your jigsaw pieces, making sure everything comes together smoothly and efficiently. Keep those words flowing, and remember, sometimes it's the simple tools that make all the difference!
27. Recursive Functions and Memoization
Normal: Imagine working on a massive jigsaw puzzle, but every time you need a specific piece, you dump out the entire box and search from scratch. Seems inefficient, right? That’s how typical recursive functions can sometimes behave. Let’s take the classic Fibonacci sequence as an example:
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)This function works, but as n grows, it becomes terribly slow because it recalculates values it has already determined multiple times.
Pro Trick: Now, picture having a helper beside you who remembers exactly where each piece of the puzzle is. Whenever you need a piece, they instantly hand it over. This would save you a ton of time and energy, right? That’s the magic of memoization in action:
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)By simply adding the @lru_cache decorator from functools, our Fibonacci function now stores results of its previous calculations. This means it doesn't need to redundantly compute them again, giving us a huge boost in performance. It's like having a trusty sidekick who always has your back, ensuring you solve problems in the smartest way possible!
28. Asynchronous Programming with asyncio
Normal: Think about reading a book but pausing every single time you come across a word you don’t recognize. You stop, pull out a dictionary, find the word, understand it, then continue reading. This is similar to synchronous code — it stops and waits for each task to complete before moving to the next. For example, if we had a function that simulates waiting:
import time
def do_something():
print("Start task")
time.sleep(1)
print("End task")
start_time = time.time()
for _ in range(3):
do_something()
end_time = time.time()
print(f"Duration: {end_time - start_time} seconds")This program waits for each do_something() to finish before starting the next, taking roughly 3 seconds in total.
Pro Trick: Now, imagine reading that same book, but whenever you encounter a tricky word, you simply jot it down and continue reading. Later, you review all your jotted words simultaneously. This is the spirit of asynchronous code. You don’t get stuck on one task; you keep progressing and handle multiple tasks concurrently:
import asyncio
async def do_something():
print("Start task")
await asyncio.sleep(1)
print("End task")
start_time = time.time()
asyncio.run(asyncio.gather(do_something(), do_something(), do_something()))
end_time = time.time()
print(f"Duration: {end_time - start_time} seconds")With asyncio, the tasks run almost concurrently, making the total duration just a bit over 1 second, rather than 3. Just like efficient reading, it's all about maximizing productivity without needless waiting!
29. Abstract Base Classes (ABCs)
Normal: Picture this. You’ve crafted a beautiful base class, hoping that every subclass would implement those key methods you’ve envisioned. But lo and behold, as your codebase grows, and multiple developers jump in, they miss out on some of those crucial methods. Now you’ve got a handful of subclasses acting like rebels, and you’re left pulling your hair out, wondering where it all went wrong.
class Fruit:
def taste(self):
pass
class Apple(Fruit):
pass
# Oops! Forgot to implement the taste method for ApplePro Trick: Enter the world of Abstract Base Classes, fondly known as ABCs. Think of ABCs as the wise old guardian of your codebase. They ensure that any class inheriting from them must implement specific methods, no exceptions allowed. By marking a method as abstract, you’re laying down the law, ensuring that every child class plays by the rules.
from abc import ABC, abstractmethod
class Fruit(ABC):
@abstractmethod
def taste(self):
pass
class Apple(Fruit):
def taste(self):
return "Sweet and crisp!"With ABCs in the mix, if someone tries to get sneaky and not implement an abstract method, Python won’t let them off easy. It’ll raise a fuss (and by that, I mean an error) until they toe the line. So, next time you wish for a bit of discipline in your class hierarchy, remember the watchful gaze of ABCs. They’re here to make sure your classes stay in harmony and sing the right notes!
30. Efficient String Formatting with f-strings
Normal: We’ve all been there. You have a couple of variables, and you want to stitch them into a neat little message. So, you pull out your trusty % or the .format() method. It's like piecing together a jigsaw puzzle, but sometimes you just can't seem to find that one piece that fits right.
name = "Alice"
age = 30
message = "Hello, my name is %s and I'm %d years old." % (name, age)
# OR
message = "Hello, my name is {} and I'm {} years old.".format(name, age)Pro Trick: Now, imagine if that jigsaw puzzle came with a guide. Introduced in Python 3.6, f-strings are precisely that guide for string formatting. They’re like that cool, efficient friend who just knows how to make things work. With f-strings, you can directly embed expressions inside string literals. It’s simple, it’s elegant, and boy, is it fast!
message = f"Hello, my name is {name} and I'm {age} years old."It’s like magic! No more juggling with positions or arguments. The f-string method seamlessly integrates variables into your strings, making your code not only cleaner but also more intuitive. So, the next time you’re piecing together strings, remember f-strings and let them weave their magic. String formatting has never been this breezy!
31. Using set for Quick Membership Tests
Normal: Imagine hosting a large garden party. Guests are arriving, and you’re checking off names from a long list. It’s going well, but as the list grows, you find yourself scanning up and down, trying to find each name. This is akin to using lists or loops for membership tests in Python. It gets the job done, but it’s not the most efficient, especially as the guest list (or your data) grows.
guest_list = ['Emma', 'Lucas', 'Olivia', 'Liam', 'Ava', ...] # and many more
def is_invited(guest):
return guest in guest_list
# Checking if 'Mason' is invited
if is_invited('Mason'):
print("Mason is on the guest list!")
else:
print("Mason didn't get an invite.")Pro Trick: Now, imagine if you had a magical guestbook. As soon as a guest approached, the book instantly tells you if they’re invited. This is the power of the set data structure in Python. It's like your very own guestbook wizard! With set, checking for membership becomes lightning fast, thanks to its average-time complexity of O(1).
guest_set = {'Emma', 'Lucas', 'Olivia', 'Liam', 'Ava', ...} # and many more
# Checking if 'Mason' is invited
if 'Mason' in guest_set:
print("Mason is on the guest list!")
else:
print("Mason didn't get an invite.")No more scanning lengthy lists. With set, you get instant answers. So, the next time you're faced with a hefty membership test, turn to your trusty set data structure. It's like having a little wizard in your code, always ready to assist in the blink of an eye!
32. Multi-threading the Pythonic Way
Normal: Picture a bustling kitchen during dinner rush. Chefs are running around, trying to multitask between chopping, frying, and plating. Things can get chaotic pretty fast, and the head chef is on edge. The risk? Overcooked pasta and unhappy customers. This scenario reminds us of what it’s like when trying to handle multi-threading without the right tools. A bit like juggling flaming torches!
import time
import _thread as thread
def worker(number):
time.sleep(2)
print(f"Worker {number} has finished!")
# Starting two threads
thread.start_new_thread(worker, (1,))
thread.start_new_thread(worker, (2,))
time.sleep(4) # Wait for both threads to finish
print("All workers completed!")Pro Trick: Now, let’s reimagine that kitchen, but this time there’s a system in place. Chefs move in harmony, there’s a dedicated place for each task, and everything just… flows. This harmonious dance is what Python’s threading module brings to the table. It's like having a seasoned chef guiding the kitchen staff, making multi-threading a breeze.
import time
import threading
def worker(number):
time.sleep(2)
print(f"Worker {number} has finished!")
# Starting two threads
thread1 = threading.Thread(target=worker, args=(1,))
thread2 = threading.Thread(target=worker, args=(2,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print("All workers completed!")With the threading module, you've got more control. Thread management becomes intuitive, allowing you to focus on the task at hand rather than the intricacies of thread synchronization. The next time you're diving into the world of multi-threading, think of the harmonious kitchen. Bring in Python's threading module, and let it choreograph the dance of your threads!
33. Type Checking with isinstance
Normal: Envision an art collector meticulously examining each painting at a gallery, using just one lens to authenticate every piece. While the lens may work for specific paintings, it’s not versatile enough for the variety in the gallery. Similarly, using Python’s type() function is like using that one lens - while it can get the job done, it's not the most flexible way to check an object's type.
number = 5
if type(number) == int:
print("It's an integer!")
else:
print("It's not an integer.")Pro Trick: Now, imagine our art collector equipped with a multi-lens magnifier, able to verify paintings of various styles and ages with precision. This is the power that isinstance() brings to Python. It's like a knife for type checking. Not only can it check an object against a single type, but it can also validate it against multiple types in one go!
number = 5
if isinstance(number, (int, float)):
print("It's a number!")
else:
print("It's not a number.")With isinstance(), you're equipped to handle more complex scenarios with ease. It's designed to make your code more robust and adaptable, so you don't get caught off guard. The next time you're checking types in Python, remember the art collector and their trusty multi-lens magnifier. Opt for isinstance(), and make your type checks a masterpiece!
34. Using Virtual Environments
Normal: Picture a grand library, where all the books, from every genre and era, are jumbled together on the same shelf. It’s a treasure trove, but trying to find a specific book becomes a chore. You might accidentally pull out a science fiction novel when you’re looking for a 19th-century romance. Installing Python packages system-wide is akin to this chaotic library. While everything is in one place, overlapping requirements and versions can lead to messy conflicts.
# Installing a package system-wide
pip install some_packagePro Trick: Now, envision the same library, but this time it’s organized into dedicated rooms. A room for fiction, one for biographies, another for travel. Each room offers a curated experience. This is the beauty of virtual environments in Python. By using tools like venv or virtualenv, you're essentially creating 'rooms' for each of your Python projects, ensuring that their dependencies remain separate and conflict-free.
# Creating a new virtual environment using venv
python -m venv my_project_env
# Activating the virtual environment
source my_project_env/bin/activate # On Windows, it's: my_project_env\Scripts\activate
# Now, install packages within this isolated environment
pip install some_packageWith virtual environments, managing dependencies becomes a walk in the park. It’s like having a personal librarian who ensures that every book (or package) is in its rightful place, ready for you when you need it. So, the next time you’re kicking off a new Python project, remember the well-organized library. Set up a virtual environment, and keep your code’s dependencies neat, tidy, and conflict-free!
35. Organized Imports with from x import y
Normal: Imagine you’re a chef preparing a gourmet meal. Instead of picking just the spices you need, you lug the entire spice rack over to the stove. It’s cumbersome, and you end up sifting through dozens of spices just to find the two or three you need. In Python, importing entire modules when you only need a fraction of their functionalities is like hauling that entire spice rack. It might get the job done, but it’s not the most efficient way.
import math
angle = 45
sine_value = math.sin(math.radians(angle))Pro Trick: Now, picture the same kitchen, but this time, you’ve got a nifty spice tray, holding just the spices you need for that particular dish. It’s right there, making your cooking streamlined and efficient. This is what the from x import y syntax offers. By importing just the specific functionalities you need, your code becomes clearer and you might even see a performance boost.
from math import sin, radians
angle = 45
sine_value = sin(radians(angle))This method of importing helps you keep track of what’s really essential to your code, cutting out the fluff. Just like a chef with their handy spice tray, you’re equipped to create a masterpiece without any unnecessary hassle. The next time you’re writing Python code, think of that chef: streamline your imports, focus on what you truly need, and whip up some elegant code!
36. Profiling and Optimizing with cProfile
Normal: Consider you’re an archaeologist, exploring an ancient city. Instead of having a detailed map or tools, you’re relying on your instincts, wandering around hoping to stumble upon treasures. This method might land you some discoveries, but you’re likely to miss out on a lot. In the realm of Python, trying to identify performance bottlenecks through guesswork or rudimentary timing is similar to this aimless exploration. You might spot some issues, but many would remain undiscovered.
import time
def slow_function():
total = 0
for i in range(1000000):
total += i
return total
start_time = time.time()
slow_function()
end_time = time.time()
print(f"Function took {end_time - start_time} seconds to run.")Pro Trick: Now, imagine our archaeologist equipped with state-of-the-art sensors and a detailed map of the city. Every alleyway, every nook and cranny is accessible, making discoveries more systematic and insightful. This is what cProfile brings to your Python performance tuning. It offers a comprehensive breakdown of where your code spends its time, guiding you directly to performance hotspots.
import cProfile
def slow_function():
total = 0
for i in range(1000000):
total += i
return total
profiler = cProfile.Profile()
profiler.enable()
slow_function()
profiler.disable()
profiler.print_stats(sort="cumulative")Armed with this in-depth analysis, optimizing your code becomes more of a science than a guessing game. It’s like having a trusty guide on your journey to code optimization, ensuring you take the right paths and make informed decisions. So, the next time your Python code feels sluggish, remember our well-equipped archaeologist. Call upon cProfile, uncover those performance treasures, and transform your code into a lean, mean, executing machine!
37. Multidimensional Lists with List Comprehensions
Normal: Think of a young artist painting a mural, stroke by stroke, taking hours to sketch each detail, layer by layer. While it’s a labor of love, it demands patience and time. Similarly, when you craft multidimensional lists in Python using nested loops, you’re meticulously constructing your data structure, piece by piece.
rows, cols = 3, 3
matrix = []
for i in range(rows):
row_list = []
for j in range(cols):
row_list.append((i, j))
matrix.append(row_list)
print(matrix) # Outputs: [[(0, 0), (0, 1), (0, 2)], [(1, 0), (1, 1), (1, 2)], [(2, 0), (2, 1), (2, 2)]]Pro Trick: Now, let’s reimagine our artist with a modern spray paint tool. With sweeping gestures, they can paint vast sections of the mural in a fraction of the time, achieving the same result but far more efficiently. Nested list comprehensions in Python are akin to this tool. They let you create multidimensional structures in a more compact and expressive way.
rows, cols = 3, 3
matrix = [[(i, j) for j in range(cols)] for i in range(rows)]
print(matrix) # Outputs: [[(0, 0), (0, 1), (0, 2)], [(1, 0), (1, 1), (1, 2)], [(2, 0), (2, 1), (2, 2)]]By harnessing the power of nested list comprehensions, you’re not just making your code shorter; you’re making it more readable and expressive. It’s like a breath of fresh air, transforming the way you approach data structures in Python. So, the next time you’re looking to craft multidimensional lists, channel the spirit of our modern artist. Opt for list comprehensions, and watch as your code becomes a beautiful masterpiece of brevity and clarity!
38. Dependency Management with poetry:
Normal: Let’s envision a librarian meticulously cataloging books. They write down each book’s details in a large ledger, noting down every new arrival and ensuring no book is left out. This is a painstaking, manual process. Similarly, managing dependencies in Python projects using pip and a requirements.txt file often feels like this. Every library, every version needs to be jotted down, and maintaining this can get challenging.
pip install requests==2.25.1
echo "requests==2.25.1" >> requirements.txtPro Trick: Now, imagine the same library, but the librarian has a state-of-the-art digital catalog system. They scan a book, and voila! All its details are logged, cross-referenced, and neatly organized. Welcome to the world of poetry. With poetry, managing dependencies becomes a breeze. It not only tracks your dependencies but also ensures they play well together, making the entire process smoother.
poetry add requestsWhat’s even more magical? It prepares your project for publishing with just a few commands. This means less time wrestling with setup files and more time focusing on what truly matters: writing great code.
Transitioning to poetry is like moving from the manual ledgers of old to a smart catalog system. It brings order, efficiency, and a touch of modernity to your Python projects. So, the next time you're setting up a project or grappling with dependency chaos, remember our high-tech librarian. Opt for poetry, and let your project management be as poetic as your code!
39. Using zip for Parallel Iteration
Normal: Imagine you’re at a bustling farmers’ market. You have two baskets, one with fresh apples and another with oranges. To make a mixed fruit bag for your customers, you individually pick one fruit from each basket, double-checking to ensure you don’t miss any. This approach, while thorough, feels a bit roundabout and time-consuming, doesn’t it? The same goes for using indices or loops to iterate through multiple lists in Python.
fruits1 = ["apple", "banana", "cherry"]
fruits2 = ["orange", "blueberry", "grape"]
for i in range(len(fruits1)):
print(fruits1[i], fruits2[i])Pro Trick: Now, imagine a seasoned vendor joining you with a special tool that clamps an apple and an orange together, letting you pick both simultaneously. What a game-changer! In Python, the zip function is that magical tool. It pairs items from multiple lists, letting you iterate over them concurrently, without the fuss of indices.
for fruit1, fruit2 in zip(fruits1, fruits2):
print(fruit1, fruit2)With zip, your code becomes not only more concise but also more intuitive. You no longer need to juggle indices or balance multiple loops. You can focus on the heart of your logic, knowing that zip has got your back, aligning everything perfectly. So, the next time you find yourself managing multiple lists, think of our savvy vendor. Use zip, and let your iterations flow effortlessly, just like a gentle stroll through a serene market!
40. Dictionary get Method for Default Values
Normal: Recall the days when you would eagerly wait by the mailbox, hoping to find a letter from a friend. Every time you’d approach, there was this ritual: Open the mailbox, check for the letter, and if it’s not there, maybe leave a small note asking the mailman if they missed it. This constant checking and double-checking, while necessary, was tiring. Similarly, in Python, continuously verifying the existence of a key in a dictionary before retrieving its value can feel like this recurring mailbox check.
data = {"name": "John", "age": 30}
if "address" in data:
address = data["address"]
else:
address = "Address not found"Pro Trick: But what if you had a magic mailbox that automatically gave you a friendly note when your expected letter wasn’t there? It’d be splendid, wouldn’t it? Python’s get method on dictionaries acts just like that. It allows you to specify a default value if the key you're looking for isn't present. No more tedious checks!
address = data.get("address", "Address not found")With the get method, your interactions with dictionaries become more streamlined and error-free. The need to brace for potential KeyErrors diminishes, leaving your code cleaner and more direct.
So, the next time you’re reaching into the vast mailbox of data dictionaries, remember there’s a friendly tool waiting to make your experience pleasant. Embrace the get method, and enjoy a more predictable and harmonious dance with your dictionaries!
41. Contextlib’s supress for Graceful Failure
Normal: Imagine you’re setting up dominoes, attempting to craft a dazzling chain reaction. However, there’s always that risk: a slight tremor of your hand, or an unexpected breeze, and a domino might tumble prematurely. You’d then carefully reset it, ensure everything’s stable, and cautiously proceed. This is analogous to handling exceptions in Python with elaborate try-except blocks. You prepare for the expected hiccups but also try to keep the sequence going smoothly.
data = {"name": "John"}
try:
age = data["age"]
except KeyError:
age = None
try:
address = data["address"]
except KeyError:
address = NonePro Trick: But what if you had a magic wand, which, with a single wave, made those errant dominoes glide back into place without a fuss? contextlib.suppress is kind of like that wand in Python's exception-handling world. It allows you to gracefully and compactly ignore specified exceptions.
from contextlib import suppress
with suppress(KeyError):
age = data["age"]
with suppress(KeyError):
address = data["address"]With suppress, your code becomes more focused on its primary logic rather than the scaffolding around exception handling. It's a tool that whispers, "It's okay if this goes awry; I've got it covered."
So, the next time you’re setting up your dominoes of logic and expecting a few potential missteps, remember that there’s a graceful tool to help you keep the chain reaction running smoothly. Bring out contextlib.suppress and let your code progress with elegance and confidence!
42. Dive into Descriptors
Normal: Think of a charming garden where you’ve planted a variety of flowers. You water them, provide ample sunlight, and hope they grow healthily. However, they’re susceptible to the whims of nature. Maybe a mischievous squirrel decides to nibble on a bud or an unexpected frost damages a bloom. Similarly, in Python, when you use simple attributes for object properties, they’re left exposed and vulnerable to unintended changes.
class Garden:
def __init__(self):
self.flower = "Rose"Usage:
g = Garden()
g.flower = "Dandelion" # Suddenly, our rose garden has a dandelion!Pro Trick: What if there was a magical fence you could set up around each flower, which would not only protect it but also allow it to thrive under certain conditions? In Python, descriptors offer this kind of protection and customization for object properties.
class FlowerDescriptor:
def __init__(self, value):
self._flower = value
def __get__(self, instance, owner):
return self._flower
def __set__(self, instance, value):
if value == "Rose":
self._flower = value
else:
print("Only roses are allowed in this garden!")
class Garden:
flower = FlowerDescriptor("Rose")Usage:
g = Garden()
g.flower = "Dandelion" # Outputs: Only roses are allowed in this garden!
print(g.flower) # Outputs: RoseWith descriptors, you gain a granular control over how attributes of an object are accessed and modified. They serve as guardians, ensuring that your attributes remain pristine, but also flexible enough to adjust when the situation demands.
The next time you’re looking to cultivate a garden of code, consider using descriptors to give your properties the nurturing environment they deserve. With these tools, you can ensure your garden remains vibrant, no matter what challenges nature (or mischievous squirrels) might throw at it!
43. Just-in-time Compilation with Numba
Normal: Picture this: You’ve got this beautiful classic car. It’s gorgeous to look at and has that nostalgic aura that just can’t be matched. But when it comes to speed, well… modern race cars would leave it in the dust. Similarly, Python is renowned for its readability and expressiveness, but it’s often slower than its compiled counterparts.
For a simple example, imagine calculating the factorial of a number:
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n-1)
# Time taken is dependent on Python's interpretation speed.
result = factorial(10)Pro Trick: What if you could give that classic car a modern engine upgrade, transforming it into a roaring beast on the road, without losing its vintage charm? That’s exactly what Numba does for Python. It’s like a turbo boost, leveraging just-in-time compilation to give Python the edge it often lacks in execution speed.
from numba import jit
@jit(nopython=True)
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n-1)
# With Numba's touch, this runs blazingly fast!
result = factorial(10)By using Numba, you’re essentially allowing your Python code to be transformed into optimized machine code at runtime. It’s like having a co-pilot who instantly upgrades your car’s engine mid-journey whenever you hit a tough uphill climb.
So, the next time you’re fondly coding in Python and yearn for that extra dash of speed, remember that Numba’s got your back. It ensures you don’t have to compromise between Python’s elegance and the need for speed. Happy coding and faster results, all in one!
44. Implementing Singletons
Normal: Imagine you’ve just started a book club. Anyone can join, but there’s only one golden rule: one book per month. No more, no less. In the excitement, multiple members end up buying the same book for the club, resulting in unnecessary duplicates. Similarly, in our code, we might unintentionally create multiple instances of a class when we actually wanted just one.
Here’s an example of a class where multiple instances can unintentionally be created:
class BookClub:
def __init__(self, book):
self.book = book
# Different instances with the same data.
club1 = BookClub("Pride and Prejudice")
club2 = BookClub("Pride and Prejudice")Pro Trick: Now, let’s circle back to our book club. What if there was a librarian who ensures that only one copy of the chosen book is acquired, no matter how many members offer to buy it? This “one source of truth” approach is exactly what the Singleton design pattern offers in software development.
Using metaclasses, we can guarantee a class has only one instance:
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class BookClub(metaclass=SingletonMeta):
def __init__(self, book):
self.book = book
# Both point to the same instance.
club1 = BookClub("Pride and Prejudice")
club2 = BookClub("Moby Dick")
print(club1.book) # Outputs: "Pride and Prejudice"
print(club2.book) # Outputs: "Pride and Prejudice" as wellWith this nifty trick, just like our book club librarian, we ensure there’s only one instance of our class, regardless of how many times we try to instantiate it. This pattern proves that sometimes, less truly is more. So the next time you’re looking for that “one source of truth” in your code, remember the Singleton’s streamlined elegance. It’s a game changer!
45. Cython for C-level Performance
Normal: Think of a standard bicycle. It gets you from point A to B, reliably. You pedal along, enjoying the scenery, and while it may not be the fastest mode of transport, it’s versatile and gets the job done. This is a lot like writing pure Python code. Python is straightforward, highly readable, and flexible. However, when it comes to sheer speed, sometimes you wish your bicycle had a bit more oomph.
Here’s an example of pure Python code to compute the factorial of a number:
def factorial(n):
if n == 0:
return 1
return n * factorial(n-1)
print(factorial(5)) # Outputs: 120Pro Trick: Now, imagine giving your bicycle an engine and turning it into a speedy motorbike. Suddenly, those long distances don’t seem that far anymore. This is what Cython does to your Python code. It takes your reliable bicycle and supercharges it, enhancing your code’s execution speed by converting it into C.
With Cython, the same factorial function can be written as:
def factorial(int n):
if n == 0:
return 1
return n * factorial(n-1)This might look quite similar, but when compiled with Cython, it’s transformed into C code under the hood, and the execution speed can be significantly faster.
With Cython, you don’t need to dive deep into C or C++ to get that extra boost of speed. It’s like getting a motorbike without needing a mechanic’s skills. So, when Python isn’t zippy enough for your needs, remember that Cython has got your back. It gives you the thrill of speed with the ease of Python!
46. Function Overloading with singledispatch
Normal: Imagine you’re at a coffee shop, and you order a coffee. But what if you want it iced? Or with almond milk? Or a double shot? Typically, you’d have to specify each variation, making the ordering process a bit cumbersome. Similarly, in coding, when functions need to handle different types of inputs, we often find ourselves writing multiple versions of the same function or stuffing them with conditional logic.
Here’s a simplified example where you might want to display a message based on input type:
def display_message(data):
if isinstance(data, int):
print(f"Received an integer: {data}")
elif isinstance(data, str):
print(f"Received a string: {data}")
# ... and so on for other types
display_message(5) # Outputs: Received an integer: 5
display_message("Hi") # Outputs: Received a string: HiPro Trick: But what if ordering coffee could be as simple as just stating your preference, and the barista intuitively knows how to make it? That’s where Python’s functools.singledispatch steps in for our coding scenario. It allows functions to act differently based on the type of the first argument, making your code elegant and readable.
Using singledispatch, our previous example becomes:
from functools import singledispatch
@singledispatch
def display_message(data):
pass
@display_message.register(int)
def _(data):
print(f"Received an integer: {data}")
@display_message.register(str)
def _(data):
print(f"Received a string: {data}")
display_message(5) # Outputs: Received an integer: 5
display_message("Hi") # Outputs: Received a string: HiWith singledispatch, your code behaves much like that intuitive barista. You state your requirement, and the function knows just how to respond. It's a refreshing way to make your functions more adaptive and tidy, all while sipping on that perfectly-made coffee. Cheers to elegant coding!
47. Making Use of else in Loops
Normal: Think of a time when you’re scanning a bookshelf looking for a particular book. You move book by book, and if you find it, you stop searching. After the search, you might then wonder, “Did I make it through the entire shelf or stop early?” Typically, in code, we’d use flags to keep track of such scenarios.
Here’s a coding example, looking for a special number in a list:
numbers = [1, 3, 5, 7, 9]
special_number = 4
found = False
for number in numbers:
if number == special_number:
found = True
break
if not found:
print("Special number not found!")Pro Trick: Now, imagine a magical bookshelf that gives a little beep if you’ve checked all the books without finding the one you’re searching for. That’s what Python’s else in loops feels like! It's a neat feature that many might not be aware of. It runs the code in the else block only if the loop wasn't interrupted by a break.
Using the else clause, our previous code becomes:
numbers = [1, 3, 5, 7, 9]
special_number = 4
for number in numbers:
if number == special_number:
break
else:
print("Special number not found!")With this approach, there’s no need for the extra ‘found’ flag. Python gives us a direct and clear way to state our intentions. It’s like that nifty bookshelf feature, making our tasks just a tad bit smoother. Happy reading (or coding, in this case)!
48. Monkey Patching — With Caution
Normal: Picture yourself trying to fit your old-school CD player into a modern car that’s only equipped for Bluetooth. Instead of finding a new solution, you might think, “Wouldn’t it be great if I could just tweak the car’s system to accept my CD player?” In the software realm, this reluctance to adapt sometimes leads us to live with the constraints of a library or module because, well, they were designed in a certain way, right?
For instance, let’s say there’s a function in a library that adds two numbers:
# In some external library
def add(a, b):
return a + bBut for some reason, you want it to always add 5, no matter what the second argument is.
Pro Trick: Enter Monkey Patching! It’s the coding equivalent of tweaking the car’s system. You can temporarily change the behavior of a function or method from a module without altering the original code. But remember, with great power comes great responsibility. This method should be used judiciously.
Using our example:
import external_library
# Original behavior
print(external_library.add(3, 2)) # Output: 5
# Monkey patch
def new_add(a, b):
return a + 5
external_library.add = new_add
# Modified behavior
print(external_library.add(3, 2)) # Output: 8While this is an oversimplified example, the core idea stands: monkey patching can alter behaviors in surprising ways. So while it’s a nifty tool to have in your belt, it’s crucial to understand the potential side effects. Use it sparingly and always document your reasons. Consider it like doing a DIY project on a rented property; it might serve your purpose, but make sure you’re not affecting the property’s foundation!
49. Dynamic Attribute Access with getattr and setattr
Normal: Imagine you’re rummaging through your old diary entries and each entry is tagged with a specific emotion. If you knew exactly which emotion you wanted to read about, you’d jump directly to that page. But what if you wanted to surprise yourself? Hardcoding emotions to search for would defeat that purpose.
In programming, this is akin to hardcoding attribute names for access or modification.
Consider this class:
class Diary:
def __init__(self):
self.happy = "I had a great day today!"
self.sad = "It was a tough day."If you wanted to fetch the happy entry, you'd typically do:
diary = Diary()
print(diary.happy) # Outputs: "I had a great day today!"Pro Trick: But what if you want a more dynamic approach? This is where getattr and setattr come in. They're like having an index to your diary entries, allowing you to choose which page (or emotion) to jump to based on a whim.
Using getattr:
emotion = "sad" # This could be dynamically set
entry = getattr(diary, emotion)
print(entry) # Outputs: "It was a tough day."And if you ever wanted to dynamically change or add an entry:
setattr(diary, "excited", "I can't believe I learned something so cool!")
print(diary.excited) # Outputs: "I can't believe I learned something so cool!"By making your code more dynamic with getattr and setattr, you not only make it more flexible but also open the door to some innovative applications. It's like turning your static diary into an interactive journal!
50. Lazy Evaluation with Generators
Normal: Remember the days when we had to wait for photos to be developed before we could see them? Or when we’d load a webpage and wait forever for every image and video to load before we could even read the text? That’s a bit like loading an entire dataset into memory before processing it. Sure, it works, but it can be inefficient, especially when you don’t need all the data at once.
Here’s a quick example:
def fetch_records(num):
records = []
for i in range(num):
records.append(i)
return records
data = fetch_records(1000000)
for record in data:
# Process the record
passThis function fetches a million records and stores them all in memory.
Pro Trick: But what if, like flipping through a photo album, you only wanted to view one photo (or record) at a time? Generators in Python allow you to do just that. They give you one item at a time and only compute the next when you ask for it, preserving memory.
Using a generator:
def fetch_records(num):
for i in range(num):
yield i
data = fetch_records(1000000)
for record in data:
# Process the record
passBy changing the function to use yield instead of appending to a list, you've turned fetch_records into a generator. Now, the function produces records on-the-fly and doesn't hold a million integers in memory.
Think of generators as a photo streaming service, showing you one photo at a time, without the need to wait for the entire album to download. Efficient, isn’t it?
51. Complex Numbers and Math in Python
Normal: Back in the day, when we wanted to deal with complex numbers, we’d either reach for a specialized calculator or, heaven forbid, pull out paper and pen to manually crunch the numbers. Imagine having to implement the math for complex numbers each time you needed them in a program:
def add_complex(x1, y1, x2, y2):
real_part = x1 + x2
imaginary_part = y1 + y2
return (real_part, imaginary_part)
result = add_complex(3, 2, 1, 7)
print(f"Sum is: {result[0]} + {result[1]}i")Here, we’ve set up a simple function to add two complex numbers. It works, but it’s a bit tedious if you ask me.
Pro Trick: But, you know, Python always has a trick up its sleeve! Python inherently supports complex numbers, making arithmetic operations straightforward:
z1 = 3 + 2j
z2 = 1 + 7j
result = z1 + z2
print(f"Sum is: {result.real} + {result.imag}i")Notice the “j” notation? That’s Python’s way of representing the imaginary part of a complex number. And the best part? You can do all standard operations — addition, subtraction, multiplication, even division — with no fuss.
It’s like having a powerful, dedicated calculator right there in your Python toolkit. Time-saving and elegant!
52. Fluent Interfaces for Readable Code
Normal: In the old times, when chaining methods together, we’d often break them apart using temporary variables. It got the job done but resulted in a rather disjointed experience:
class Bakery:
def __init__(self):
self._items = []
def add_bread(self):
self._items.append("Bread")
return self
def add_cake(self):
self._items.append("Cake")
return self
def show_items(self):
print(", ".join(self._items))
shop = Bakery()
shop.add_bread()
shop.add_cake()
shop.show_items()This style of breaking apart method chains with intermediary steps isn’t the most elegant or readable.
Pro Trick: Let’s up our game a bit. Using fluent interfaces, we can allow for a more streamlined and readable approach:
class Bakery:
def __init__(self):
self._items = []
def add_bread(self):
self._items.append("Bread")
return self
def add_cake(self):
self._items.append("Cake")
return self
def show_items(self):
print(", ".join(self._items))
Bakery().add_bread().add_cake().show_items()Notice how the methods flow naturally? By returning self from each method, we can chain methods together in a way that's very expressive and intuitive. It's like crafting a story, each method adding a little bit more to the narrative, leading to a cohesive end. Elegant coding at its finest!
53. Dunder or “Magic” Methods for Operator Overloading
Normal: Let’s think back to a time when we might have used Python objects and expected them to behave like numbers or strings. Without any customization, if we tried to add two instances of a user-defined class, Python wouldn’t know what to do:
class Box:
def __init__(self, items):
self.items = items
box1 = Box(5)
box2 = Box(3)
# This will raise an error.
result = box1 + box2In this scenario, Python is left scratching its head, wondering, “How do I add these two ‘Box’ objects together?”
Pro Trick: The beauty of Python lies in its extensibility. With dunder (double underscore) methods, or as some call them, “magic” methods, we can teach Python how to handle these operations:
class Box:
def __init__(self, items):
self.items = items
def __add__(self, other):
return Box(self.items + other.items)
def __str__(self):
return f"Box with {self.items} items"
box1 = Box(5)
box2 = Box(3)
result = box1 + box2
print(result) # Outputs: Box with 8 itemsHere, the __add__ method tells Python how to handle addition for the Box class. And just like that, we've extended the natural behavior of our objects. You can imagine the limitless possibilities when you start overriding other dunder methods. It's like giving a fresh coat of paint to an old bicycle, making it ride smoother and look cooler!
54. Static Type Checking with mypy
Normal: Recall the days when you would write Python code, basking in the flexibility of its dynamic typing. Life seemed breezy, until runtime errors due to type mismatches started crashing the party:
def add(a, b):
return a + b
result = add(10, "20") # Oops! TypeError at runtimeHere, everything seems fine until the function is called. Suddenly, trying to add an integer and a string throws a wrench in our plans, and our application grinds to a halt.
Pro Trick: Here’s where mypy waltzes in, like a vigilant guardian of your code. By adding type hints and running our code through mypy, we can catch these pesky errors even before the code runs:
def add(a: int, b: int) -> int:
return a + b
# Running mypy will flag an error without even executing the code!
result = add(10, "20") # mypy will complain about thisWith mypy, we get an early heads-up: “Hey, something’s not right here!” This proactive approach not only reduces potential runtime errors but also makes our code more readable and self-documenting. It’s like having a friendly neighbor who keeps an eye on your house when you’re away. Peace of mind, indeed!
55. Stacking Decorators for Multiple Effects
Normal: Remember the good old days, when you’d use a single decorator to give your function a slight twist? Or maybe you went a step further, diving deep into nested function calls to layer multiple effects. It felt like making a sandwich where you kept everything in separate layers, not quite mingling:
@single_effect_decorator
def my_function():
pass
# Or for multiple effects
my_function = second_decorator(first_decorator(my_function))It gets the job done, but it’s a bit like preparing a meal without mixing the ingredients — effective, but not as delightful as it could be.
Pro Trick: Now, let’s jazz things up a bit. What if you could stack your decorators, one on top of the other, letting each one add its unique flavor to the mix?
@second_effect_decorator
@first_effect_decorator
def my_function():
passWith this approach, our my_function dances through the first_effect_decorator and then swirls into the second_effect_decorator, seamlessly blending the effects of both. It's like layering a cake with multiple fillings, where each layer enriches the overall taste. A delightful coding treat, isn't it?
56. Functional Programming with functools
Normal: Picture yourself painting a landscape — traditionally, you’d sketch out the shapes, fill them in, and gradually bring the scene to life, step by step. This is the essence of the imperative programming style: you dictate ‘how’ something should be done, controlling the flow with loops and conditional statements.
def multiply(x, y):
return x * y
result = 1
numbers = [1, 2, 3, 4, 5]
for number in numbers:
result = multiply(result, number)It’s a familiar process, like the comfort of painting a scene in a way you’ve practiced many times.
Pro Trick: But imagine if your paintbrush could adapt, allowing you to express your scene more fluidly and intuitively. This is where functools comes into play, a stroke of Python’s functional programming paradigm. It provides tools like partial and reduce that help in painting the logic of your code in more expressive strokes.
from functools import reduce
def multiply(x, y):
return x * y
numbers = [1, 2, 3, 4, 5]
result = reduce(multiply, numbers)Here, reduce elegantly applies the multiply function, letting the logic flow across the list of numbers naturally. It’s like allowing the colors on your palette to blend together organically, creating a captivating and efficient masterpiece in code. Happy painting!
57. The Power of Annotations for Type Hinting
Normal: Think of coding like cooking from a recipe. If the recipe just said “add some spices”, you might be left scratching your head, wondering exactly which spices to use. Traditional Python often left us in a similar spot. We’d have functions, and from comments or docstrings, we’d get a vague idea about what type of ingredient (or data) to pass.
def add(a, b):
"""Add two numbers together.
Args:
a: int
b: int
Returns:
int
"""
return a + bLike a dish that turns out okay but leaves you wanting a little more clarity, this method gets the job done but doesn’t offer the immediacy of understanding.
Pro Trick: Now, imagine a recipe that not only tells you to add spices but specifies which ones, how much, and even the expected aroma. That’s the precision Python’s type hinting brings to the table.
def add(a: int, b: int) -> int:
return a + bJust like how the right hint of cinnamon can transform a dish, type hinting illuminates your functions, making the expected input and output types clear at a glance. It’s a small touch, but it adds a dash of clarity that can make your coding (or cooking) journey so much smoother. Happy coding and bon appétit!
58. Coroutines for Cooperative Multitasking
Normal: Picture this: you’re at a bustling train station, and every individual train operates on its own schedule, often waiting idle for its specific departure time. This is a lot like managing tasks with threads or processes. Each thread acts like an independent train, with its own set of resources, sometimes leading to a less efficient overall system due to resource contention or idle time.
import threading
def task_one():
# Do some work...
pass
def task_two():
# Do some other work...
pass
thread_one = threading.Thread(target=task_one)
thread_two = threading.Thread(target=task_two)
thread_one.start()
thread_two.start()
thread_one.join()
thread_two.join()Pro Trick: Now, envision a world where trains could cooperate. They seamlessly coordinate to use the tracks most efficiently, yielding to each other as needed. That’s the magic of coroutines. Instead of operating in isolation, tasks can “yield” control, allowing other tasks to run, and then pick up right where they left off.
def task_one():
print("Start of task one")
yield
print("End of task one")
def task_two():
print("Start of task two")
yield
print("End of task two")
coroutine_1 = task_one()
coroutine_2 = task_two()
next(coroutine_1)
next(coroutine_2)
next(coroutine_1)
next(coroutine_2)It’s like turning the chaotic hustle and bustle of the station into a harmonious ballet, with trains gracefully giving way to one another. Coroutines bring this elegance to your code, enabling you to craft fluid and efficient multitasking routines. Safe travels on your coding journey!
59. Dynamic Imports with importlib
Normal: Imagine you’re at a grand buffet. Right at the entrance, you pile up everything onto your plate — from appetizers to desserts, regardless of whether you’ll actually eat them all. This is somewhat akin to placing all your imports at the top of your Python file. You load everything up front, even if you might not use certain modules.
import math
import sys
import datetime
# Some code here...
# Maybe you only end up using 'math' and not 'sys' or 'datetime'Pro Trick: Now, what if you could just take what you need, when you need it, and then maybe go back for seconds or switch things up as the evening progresses? That’s the beauty of dynamic imports using importlib. Rather than loading all modules at the outset, you bring them in precisely when they're required.
def perform_calculation():
importlib = __import__('importlib')
math_module = importlib.import_module('math')
result = math_module.sqrt(16)
print(result)
# Here, 'math' is only imported when 'perform_calculation' is calledIt’s like having the agility to nimbly navigate that buffet, making the most of your dining (or coding) experience. Remember, it’s not just about having the resources, but knowing when and how to use them that counts. Bon appétit to your code!
60. Reflection and Introspection Capabilities
Normal: Think of a toy box. You’ve got a bunch of toys inside, but unless you open it up and manually check each toy, you won’t really know what’s in there. The same goes for Python objects; if you’re unaware of the attributes or functions they hold, you’re limited to what you know or have documented.
class ToyBox:
def __init__(self):
self.toy1 = "Teddy Bear"
self.toy2 = "Toy Car"
# We know there's a 'toy1', so we access it directly:
print(my_toybox.toy1) # Outputs: Teddy BearPro Trick: Now, let’s say the toy box had a magical lid. Every time you opened it, a list would appear, showing all the toys inside without you having to dig through. This is the magic of Python’s introspection capabilities. You can, on the fly, discover the attributes, methods, and more, of any object.
my_toybox = ToyBox()
# Using introspection to list all attributes of the 'ToyBox' object:
attributes = [attr for attr in dir(my_toybox) if not callable(getattr(my_toybox, attr)) and not attr.startswith("__")]
print(attributes) # Outputs: ['toy1', 'toy2']With this in hand, you can make your code more dynamic, adaptable, and even self-modifying! It’s like having X-ray vision for your Python objects. Now, go out there and take a peek under the hood of your code!
61. The __main__ Guard
Normal: Imagine having a recipe book that not only shows you how to cook, but actually starts baking every time you open it, even if you were just looking for a simple pancake recipe. This is what happens when you run scripts without any conditions. Every time the script is imported or accessed, the code runs.
# pancake.py
print("Making a pancake!")
def flip_pancake():
print("Flipping the pancake!")If you were to import this script in another file like so: import pancake, you'd immediately see the message "Making a pancake!" even if you just wanted to use the flip_pancake function.
Pro Trick: Instead, think of the if __name__ == "__main__": guard as a toggle switch on your mixer. It only starts mixing when you decide to flip it on. With this in place, your code only runs when you execute the script directly, but not when it's imported elsewhere.
# pancake.py
def flip_pancake():
print("Flipping the pancake!")
if __name__ == "__main__":
print("Making a pancake!")
flip_pancake()Now, if you import the script, you won’t get the “Making a pancake!” message. Instead, you have control over when certain parts of your code run. It’s like having a smart kitchen that knows when you’re ready to start baking!
62. Leveraging Weak References
Normal: Let’s think of memory in your computer as a massive library. Now, every time you create a reference to an object, it’s like you’re asking the librarian to hold onto a book for you. This can be handy, but if you forget to tell the librarian to put the book back, or if too many people ask her to hold onto different books, soon she’ll be overwhelmed, not able to serve other visitors efficiently.
class BigDataClass:
pass
# Strong reference
big_data_object = BigDataClass()
reference_list = [big_data_object]Here, reference_list holds a strong reference to big_data_object. As long as this reference exists, the big_data_object won't be discarded, even if we don't need it anymore.
Pro Trick: Enter weak references! Instead of telling the librarian to hold onto the book indefinitely, you’re just asking for a reminder of where it’s located. If nobody’s reading the book, the librarian can still put it away, freeing up space and her hands.
Using Python’s weakref module, you can create references that don't prevent Python from reclaiming objects when they're no longer needed.
import weakref
class BigDataClass:
pass
big_data_object = BigDataClass()
# Creating a weak reference to the object
weak_ref_to_big_data = weakref.ref(big_data_object)
# Now, if there are no more strong references to big_data_object,
# it can be garbage collected, even though weak_ref_to_big_data exists.With this approach, you can keep a sort of “soft tab” on objects, without hogging memory. It’s like having a tech-savvy librarian who knows exactly when to hold onto a book and when to put it back on the shelf. Efficient and smart!
63. Lambda Functions for Quick Jobs
Normal: Imagine you’re in a kitchen, cooking up a recipe. For each step, you take out a whole new appliance, even if it’s just to do a small task, like squeezing a lemon. It gets the job done, sure, but think of the time spent and the cleaning up afterwards.
def square(x):
return x * x
numbers = [1, 2, 3, 4]
squared_numbers = list(map(square, numbers))Here, we’re using the square function to square every number in a list. But it feels a tad overkill for such a straightforward operation, doesn't it?
Pro Trick: Now, imagine a nifty little multi-tool in that kitchen, one that you can use for those quick tasks and then put right back in your pocket. That’s what lambda functions are like in Python!
numbers = [1, 2, 3, 4]
squared_numbers = list(map(lambda x: x * x, numbers))With lambda functions, you can write quick, one-off functions without the need for a full function definition. They’re like those handy pocket tools: compact, efficient, and perfect for those on-the-fly tasks. Keep on cooking with code!
64. Type Annotations with typing
Normal: Let’s say you’re sharing a cherished recipe with a friend. But instead of listing the ingredients explicitly, you’re leaving little footnotes or side comments about what each ingredient might be. Your friend is left guessing, “Is that a teaspoon or a tablespoon?”
def add_numbers(a, b):
# a: int, b: int -> int
return a + bHere, we’ve left little comments about the expected types of a, b, and the return value. It does give some information, but it's not the most straightforward method.
Pro Trick: Now, think about that recipe again, but this time with clear measurements and steps, like a professional cookbook. That’s the clarity the typing module brings to Python code!
from typing import List
def add_numbers(a: int, b: int) -> int:
return a + bWith the typing module, you can annotate the types directly in the function definition. It's transparent, clear, and very much like having a well-written recipe. And just like a clear recipe leads to better cooking, clear type annotations lead to better coding! Bon appétit and happy coding!
65. The collections Module Gems
Normal: Imagine you’re organizing a huge gathering (post-pandemic, of course!). You have a big box, and you’re putting all the items: cutlery, glasses, plates, napkins, everything into this one container. It gets the job done, but oh boy, is it messy when you’re trying to find a spoon!
items = ["spoon", "spoon", "glass", "plate", "spoon"]
item_counts = {}
for item in items:
if item not in item_counts:
item_counts[item] = 0
item_counts[item] += 1This approach is akin to using Python’s standard list or dict for everything. It works, but sometimes it feels cumbersome or inefficient.
Pro Trick: Now, imagine you’ve got specialized compartments and organizers for your gathering. Spoons in one holder, glasses stacked neatly, plates in a rack — everything in its place. That’s what the collections module is like!
from collections import Counter
items = ["spoon", "spoon", "glass", "plate", "spoon"]
item_counts = Counter(items)Just look at how Counter made our life easier! And that's just one gem. With namedtuple, you can create simple classes for storing data. Using defaultdict, you no longer need to check if a key exists before adding to it.
The collections module is like that handy drawer organizer you never thought you needed, but once you have it, you wonder how you ever managed without! Time to make your coding gatherings a lot more organized and efficient.
66. List Comprehensions with Conditional Logic
Normal: Let’s think of this like organizing a bookshelf. You have a pile of books, and you’re going through each one, deciding if it’s fiction or non-fiction. If it’s fiction, you want to place it on the left side of the shelf, and if it’s non-fiction, on the right. And for those rare books that are a mix, you’re just putting them in a separate pile for now.
books = ["The Great Gatsby", "Silent Spring", "1984", "A Brief History of Time", "Brave New World"]
fiction_books = []
for book in books:
if "1984" in book or "Brave New World" in book or "The Great Gatsby" in book:
fiction_books.append(book)This method works, but it’s a bit like manually checking each book, pondering over it, then placing it on the shelf.
Pro Trick: Now, what if you had a cool gadget that automatically sorts the books for you as you go? Just scan, and it’s placed in the right spot instantly. That’s what list comprehensions with conditional logic feel like!
fiction_books = [book for book in books if "1984" in book or "Brave New World" in book or "The Great Gatsby" in book]The elegance! With just one line, the books are sorted, making our bookshelf (and code) look so much tidier. It’s like having a personal librarian assistant for your collections. Happy reading (and coding)!
67. Persistent Dictionaries with shelve
Normal: Let’s say you’ve got this secret recipe for a delicious cookie. Every time you want to bake, you manually search through your big recipe book, find the recipe, note down the ingredients, and get started. It works, but it’s a bit tedious.
# Save a dictionary into a text file
data = {"flour": "2 cups", "sugar": "1 cup", "chocolate": "500g"}
with open('recipe.txt', 'w') as file:
for key, val in data.items():
file.write(f"{key}={val}\n")
# Load data back from the text file
loaded_data = {}
with open('recipe.txt', 'r') as file:
for line in file:
key, val = line.strip().split('=')
loaded_data[key] = valThis is kind of like flipping through the pages every time, trying not to miss that one favorite recipe of yours.
Pro Trick: Now, imagine if you had a magical recipe box. You just whisper, “chocolate cookie recipe,” and poof! It’s right there in front of you, all details ready. That’s the magic of shelve. It's like a recipe box for your Python objects.
import shelve
# Store the dictionary
with shelve.open('recipe.db') as db:
db['choco_cookie'] = {"flour": "2 cups", "sugar": "1 cup", "chocolate": "500g"}
# Retrieve the dictionary
with shelve.open('recipe.db') as db:
loaded_data = db['choco_cookie']Just like that, no matter how many recipes (or objects) you have, they’re all at your fingertips. Effortless and tidy. Happy baking (and coding)!
68. Python’s Ternary Conditional Operator
Normal: It’s kind of like choosing between a soft drink and a cup of tea. You could go the traditional way, thinking:
“If I feel like something cold, I’ll have the soft drink. Otherwise, I’ll enjoy the tea.”
drink = ""
if feel_cold:
drink = "soft drink"
else:
drink = "tea"This works and is like taking a minute to ponder your decision.
Pro Trick: But, sometimes, you just know instantly what you want. It’s like an instinctive “I feel warm; I want a soft drink!” That’s where Python’s snazzy ternary conditional operator shines.
drink = "soft drink" if feel_cold else "tea"Quick, smooth, and super stylish. Cheers to making swift choices with flair!
69. Understanding GIL (Global Interpreter Lock)
Normal: Threading in Python! It’s a bit like when I tried to get multiple robotic vacuums to clean a room at the same time. You’d think more vacuums mean faster cleaning, right? But, they kept bumping into each other and sometimes even missed spots.
So, you create multiple threads in Python, assuming it’ll run faster on multi-core machines:
import threading
def some_task():
# some CPU-bound operation
pass
thread1 = threading.Thread(target=some_task)
thread2 = threading.Thread(target=some_task)
thread1.start()
thread2.start()
thread1.join()
thread2.join()But alas! It doesn’t speed up as much as you’d hoped due to those pesky collision-like issues caused by the GIL.
Pro Trick: To really kick performance into high gear, it’s essential to understand GIL’s dance steps. The GIL ensures only one thread executes Python bytecode at a time. So, for CPU-bound tasks, threading might not be the best choice. Instead, consider multiprocessing or using Python implementations like Jython or IronPython which don’t have a GIL.
import multiprocessing
def some_task():
# some CPU-bound operation
pass
process1 = multiprocessing.Process(target=some_task)
process2 = multiprocessing.Process(target=some_task)
process1.start()
process2.start()
process1.join()
process2.join()It’s like having multiple, separate robotic vacuums in different rooms — each doing its thing independently and efficiently. And that, my friend, is how you get a spotless house (or in this case, impeccable performance)!
70. The re Module for Regular Expressions
Normal: Back in the day, I recall trying to solve puzzles with my friends. Imagine a scenario where you’re handed a lengthy book and tasked to find all sentences containing the word “apple”. A simple approach would be to read every sentence, word by word, much like using basic string functions in Python:
text = "She has an apple. He has a banana. They have an apple pie."
apple_sentences = [sentence for sentence in text.split('.') if 'apple' in sentence]
print(apple_sentences)It’s straightforward, but can be time-consuming and limited for intricate patterns.
Pro Trick: Enter regular expressions, the cryptic yet powerful language for text searching! It’s like having a special magnifying glass that can spot patterns amidst a sea of text. Using Python’s re module, you can achieve the same task and more:
import re
apple_pattern = re.compile(r'\bapple\b')
apple_sentences = [sentence.group(0) for sentence in apple_pattern.finditer(text)]
print(apple_sentences)With this, you can match not just words, but intricate patterns like email addresses, URLs, phone numbers, and more. So next time you’re faced with a text puzzle, reach for that re magnifying glass. It's a game changer!
71. Dynamic Class Creation with type
Normal: Imagine you’re building a structure with LEGO blocks. Normally, you’d have a blueprint (or an instruction booklet) in front of you, and you’d follow that to create your structure. This is similar to how we generally define classes in Python. We lay out the blueprint using the class keyword and then create instances from it.
class Fruit:
def __init__(self, name):
self.name = name
apple = Fruit("Apple")
print(apple.name) # Outputs: AppleIt’s like our trusty old LEGO instruction booklet, guiding us on how our object should look and behave.
Pro Trick: But what if you could magically create new structures on the fly without a predefined blueprint? With Python, this isn’t magic, it’s the power of the type function! You can craft classes dynamically at runtime:
Fruit = type('Fruit', (), {'__init__': lambda self, name: setattr(self, 'name', name)})
banana = Fruit("Banana")
print(banana.name) # Outputs: BananaThis approach is like having a magic wand that lets you imagine a new LEGO structure and poof — it appears! While it might not be your everyday tool, it’s a nifty trick for those special occasions when you need a pinch of dynamism in your code. Just remember, with great power comes great responsibility!
72. Implementing Clean APIs with __all__
Normal: Think of your kitchen at home. You have a variety of tools, utensils, and ingredients, each with its own purpose. But not everything is meant for your guests to use or see. Imagine inviting friends over for dinner and just pointing them to the kitchen, saying, “Everything’s in there!” They might find themselves overwhelmed, accidentally using your rare truffle oil instead of regular olive oil. Similarly, when you create a Python module without specifying what’s meant to be used publicly, anyone importing it might be overwhelmed or misuse parts of it.
# kitchen.py
def oven():
return "Baking..."
def fridge():
return "Cooling..."
def _secret_recipe():
return "This is grandma's secret!"Using from kitchen import * would import oven, fridge, and even the _secret_recipe!
Pro Trick: But what if you could set up a neat little counter with only the tools and ingredients your friends should use? That’s where Python’s __all__ comes into play. It's like laying out the essentials for your guests so they can quickly make a salad without getting lost in the intricacies of your kitchen.
# kitchen.py
__all__ = ['oven', 'fridge']
def oven():
return "Baking..."
def fridge():
return "Cooling..."
def _secret_recipe():
return "This is grandma's secret!"Now, when someone uses from kitchen import *, only oven and fridge are accessible, keeping grandma's secret recipe safe! It's a neat and tidy way to ensure that your module exposes only what's meant to be used, reducing potential clutter and confusion. So the next time you set up a Python 'kitchen', remember the magic of __all__ to keep things orderly!
73. Chain Comparisons for Elegance
Normal: Imagine you’re trying to decide if you should wear a jacket when going outside. You check two thermometers: one by the window and another on your porch. If either shows the temperature below 60°F, you decide it’s jacket time. In Python, you might express this double-check like two separate inquiries:
thermometer_window = 58
thermometer_porch = 61
if thermometer_window < 60 or thermometer_porch < 60:
print("Better wear a jacket!")It’s clear and direct, like asking each thermometer one-by-one, “Is it cold?”
Pro Trick: But there’s a more elegant way. What if you could ask both thermometers at the same time, “Is it cold outside?” That’s where Python’s chained comparisons shine. They offer a concise way to check multiple conditions in a single line:
if 0 <= thermometer_window < 60 or 0 <= thermometer_porch < 60:
print("Better wear a jacket!")By chaining comparisons, you can swiftly and stylishly evaluate multiple conditions. It’s like taking a quick glance at both thermometers and instantly knowing what to wear. Dressing for the weather has never been so graceful!
74. Non-blocking IO with selectors
Normal: Let’s say you’re hosting a party where guests are asked to leave a voicemail with their favorite book recommendations. Using a traditional answering machine, you’d have each person come up, wait for the beep, leave their message, and move on. If two guests approached at the same time, one would have to wait. This process can be related to sequential IO operations, which can be a bit slow:
def get_voicemails():
messages = []
for guest in guests:
message = get_message_from_guest(guest)
messages.append(message)
return messagesThe downside here? While processing one message, your application could potentially miss out on others. It’s a bit like making all your guests form a single file line to leave their voicemails.
Pro Trick: Now, imagine you’ve upgraded your party setup! You’ve got multiple voicemail machines, and guests can come up to any of them to leave their messages concurrently. No more waiting in line! This is the magic of non-blocking IO with selectors.
import selectors
selector = selectors.DefaultSelector()
def process_message(key, mask):
message = key.fileobj.recv(1000)
if message:
print("Received:", repr(message))
else:
selector.unregister(key.fileobj)
key.fileobj.close()
# Assuming 'sock' is a socket already set for non-blocking
sock.setblocking(False)
selector.register(sock, selectors.EVENT_READ, process_message)
while True:
for key, mask in selector.select():
callback = key.data
callback(key, mask)By embracing the selectors module, your application can listen to multiple sources of input and respond immediately when data is available. No more missed messages or making guests wait. Everyone gets to share their favorite books at the same time. Efficient and seamless, just like a well-hosted party!
75. MRO (Method Resolution Order) and Super
Normal: Let’s think of our classes like a family tree. In earlier days, when tracing family histories, it was common for folks to directly jump from one generation to another to gather stories or inheritances. This sometimes led to confusing tales or lost heirlooms, as the exact path wasn’t always clear.
class Grandparent:
def story(self):
return "Back in my day..."
class Parent(Grandparent):
def story(self):
return "When I was your age..."
class Child(Parent, Grandparent):
def get_story(self):
return Grandparent.story(self)Here, when the Child wants a story, it directly asks the Grandparent. But what if the Parent had a different tale to tell? Jumping directly can miss out on vital pieces of history!
Pro Trick: Enter the modern era with its sophisticated tools for genealogy! Rather than directly leaping from one generation to another, a clear path is traced using records and sequences. This makes the history both transparent and organized.
class Grandparent:
def story(self):
return "Back in my day..."
class Parent(Grandparent):
def story(self):
return super().story() + " and when I was your age..."
class Child(Parent, Grandparent):
def get_story(self):
return super().story()By understanding the MRO and leveraging super(), we ensure that the Child gets the full story, starting from the Grandparent to the Parent. It's a clear, well-organized way of traversing the inheritance chain, ensuring no tales get left behind. Just like a meticulously maintained family record, every generation gets its rightful place.
76. Functional Tools: map, filter, and reduce
Normal: Imagine you’re given a handful of seeds and your task is to plant them, water them, and then gather the fruits they yield. The old-school approach would be to deal with each seed one by one. Plant it, water it, and then wait for its fruit, before moving to the next.
numbers = [1, 2, 3, 4]
squared_numbers = []
for num in numbers:
squared_numbers.append(num*num)It’s a lot like our typical loop: we iterate through items one by one, applying a function. It works, but it’s a bit like tending to one plant at a time when you have an entire garden bed!
Pro Trick: Now, think of having a magic gardening tool that can plant all seeds at once, water them simultaneously, and then help you gather all fruits in one go.
squared_numbers = list(map(lambda x: x*x, numbers))Here, map allows us to "plant" our function (lambda x: x*x) to all items in the numbers list, all at once. Similarly, tools like filter can help in "selectively watering" the plants, and reduce aids in gathering all fruits to make a sumptuous jam or juice.
This functional approach doesn’t just make the code concise, but it’s like having a gardening toolkit that makes the process efficient and enjoyable. Happy gardening… I mean, coding!
77. Dictionary Comprehensions for Swift Dictionary Creation
Normal: Remember the days when you had to assemble a jigsaw puzzle, piece by piece? It was time-consuming, right? It’s the same when we manually put together dictionaries, key-value pair by key-value pair.
keys = ["apple", "banana", "cherry"]
values = [1, 2, 3]
fruit_dict = {}
for k, v in zip(keys, values):
fruit_dict[k] = vIt’s a bit like setting up a lemonade stand by building the table first, then placing each glass, and then pouring the lemonade into each glass, one by one.
Pro Trick: What if I told you there’s a swifter way? Like having a pre-packed lemonade stand that pops up instantly.
fruit_dict = {key: value for key, value in zip(keys, values)}Just like that, dictionary comprehensions let us set up our ‘lemonade stand’ in a snap. You’re not just saving time; you’re making your code look clean and refreshing, like a glass of cold lemonade on a hot day. Cheers to efficient coding!
78. Embrace Sets for Unique Value Operations
Normal Approach: Imagine you’re given a task to collect all the unique colors from a bunch of marbles. Going the usual way, you’d probably take a basket, and every time you see a new color, you check the entire basket to make sure you haven’t added that color before. If not, you add it. Sounds exhausting, right?
marbles = ["red", "blue", "green", "red", "yellow", "blue"]
unique_colors = []
for color in marbles:
if color not in unique_colors:
unique_colors.append(color)
print(unique_colors)This will print: ['red', 'blue', 'green', 'yellow']. But the downside? You keep checking the list repeatedly for duplicates.
Pro Trick: Now, let’s shake things up a bit. Instead of the lengthy process, you decide to use a magic box (a set!) where you just throw in all the marbles, and the box automatically keeps only the unique colors. Neat, right?
marbles = ["red", "blue", "green", "red", "yellow", "blue"]
unique_colors = set(marbles)
print(unique_colors)Boom! It outputs: {'red', 'yellow', 'green', 'blue'}, showcasing the unique colors without all that tedious checking. By embracing sets, you've not only saved time but also made your coding journey a tad bit smoother. Efficiency at its finest!
79. The with Statement for Resource Management
Normal Approach: Let’s picture a scenario. You’re reading a fascinating book in a library, but every time you open a book, you need to note down the time, and when you’re done, you make another entry. It’s like manually opening and closing a file in Python:
file = open("example.txt", 'r')
content = file.read()
print(content)
file.close()It works, but there’s always a risk. What if you forget to make that ‘closing’ entry, or in this case, forget to close the file? You’re going to leave resources hanging, and over time, it might become an issue.
Pro Trick:
But what if the library had an automatic door that would note your entry and exit times without you doing anything? You just walk through, and everything’s handled. That’s what the with statement in Python is like. It ensures that the resources, like our file here, are properly managed:
with open("example.txt", 'r') as file:
content = file.read()
print(content)And just like that, the file is automatically closed once you’re done reading. No need to worry about forgetting or making manual entries. It’s a neat little trick that not only makes the code cleaner but also ensures you’re always on the right track, no matter how engrossed you are in that book or code!
80. Tuple Unpacking for Multiple Assignment
Normal Approach: Imagine you’re organizing your study desk, and you have three pens: a red one, a blue one, and a green one. You pick up each pen individually and assign them to their designated slots. Similarly, in Python, you can assign values to variables one by one:
red_pen = "red"
blue_pen = "blue"
green_pen = "green"It’s straightforward and gets the job done, but it feels a bit tedious, doesn’t it?
Pro Trick: Now, think about having a pen holder, where you can simply drop the pens in and they automatically go to their respective slots. This convenience is similar to tuple unpacking in Python:
red_pen, blue_pen, green_pen = "red", "blue", "green"In a single line, all three pens (or variables) are sorted out! This makes the code concise and visually pleasing. It’s like having an organized desk where everything just fits perfectly without the fuss. Always remember, tidying up can be both fun and efficient, whether it’s on your study desk or in your code!
81. Kudos for reaching this point! Here’s a trick to optimize your learning journey
- Normal Approach:
Diving headfirst, consuming all content in one go.
- Pro Trick:
Pace yourself. Pause, sip some water, maybe munch on an apple. Remember to give this article a clap and follow. Balancing your zeal with breaks ensures a healthier climb to the top.
82. List Insertions with bisect
Normal Approach: Imagine you’re organizing a shelf of books by their publication year, and every time you get a new book, you start from one end, scanning each book until you find the right spot for the new one. While this method gets the job done, it can become time-consuming as your collection grows:
def insert_in_sorted_list(sorted_list, value):
for i, existing_value in enumerate(sorted_list):
if value < existing_value:
sorted_list.insert(i, value)
return
sorted_list.append(value)
book_years = [1970, 1985, 2000, 2010, 2015]
insert_in_sorted_list(book_years, 1995)
print(book_years) # [1970, 1985, 1995, 2000, 2010, 2015]This is a linear search approach. As your collection grows, it takes longer to find the correct spot.
Pro Trick:
Now, instead of manually scanning each book, imagine having a magical bookmark that can instantly tell you where the new book belongs. This is akin to what bisect provides - a way to binary search through sorted lists:
import bisect
def insert_in_sorted_list(sorted_list, value):
position = bisect.bisect_left(sorted_list, value)
sorted_list.insert(position, value)
book_years = [1970, 1985, 2000, 2010, 2015]
insert_in_sorted_list(book_years, 1995)
print(book_years) # [1970, 1985, 1995, 2000, 2010, 2015]With bisect, even if your collection contains thousands of books, finding the right spot is swift and efficient. It's like having a librarian's intuition, but for lists. Happy organizing!
83. Currying Functions for Partial Applications
Normal Approach: Let’s say you’re cooking up a recipe that requires different spices in varying amounts. If you often use a specific mix of spices, you might create separate recipes for each variation. This can quickly become a lot to manage!
def spice_mix(chili, garlic, cumin, paprika):
return f"{chili}g chili, {garlic}g garlic, {cumin}g cumin, {paprika}g paprika"
# If you often use 5g chili, 2g garlic, 3g cumin
def special_spice_mix(paprika):
return spice_mix(5, 2, 3, paprika)
print(special_spice_mix(4)) # "5g chili, 2g garlic, 3g cumin, 4g paprika"While this approach works, it’s a bit like having to write out a whole recipe card just because you changed one ingredient.
Pro Trick: Enter currying! It’s like having a set of measuring spoons where each spoon automatically measures out the right amount of several spices, but you can still choose one spice to vary.
Using currying, you can “freeze” some arguments of your function and produce new, specialized functions:
from functools import partial
def spice_mix(chili, garlic, cumin, paprika):
return f"{chili}g chili, {garlic}g garlic, {cumin}g cumin, {paprika}g paprika"
special_spice_mix = partial(spice_mix, 5, 2, 3)
print(special_spice_mix(4)) # "5g chili, 2g garlic, 3g cumin, 4g paprika"With currying, you can effortlessly create variations of your main function without re-writing the entire thing. It’s like having a customizable spice rack at your fingertips. Bon appétit!
84. Understanding Assertions with assert
Normal Approach: Imagine you’re building a model airplane. You carefully glue each part, but every so often, you stop to visually check if the pieces are aligned correctly. If they aren’t, you fix it before proceeding.
def calculate_area(length, width):
if length < 0 or width < 0:
print("Error: Dimensions should be non-negative!")
return None
return length * width
area = calculate_area(5, -3)
if area is None:
# Handle the error, maybe stop the program or use a default value
area = 0While this method ensures you catch errors, it’s like having to inspect every part of your model airplane with a magnifying glass.
Pro Trick: Using assertions is akin to having a little alarm system on your model airplane that beeps whenever a part isn’t correctly placed.
def calculate_area(length, width):
assert length >= 0 and width >= 0, "Dimensions should be non-negative!"
return length * width
# If dimensions are negative, an AssertionError is raised, alerting you immediately.
area = calculate_area(5, -3)With the assert statement, if the condition isn't met, your code will instantly raise an error, letting you know something's amiss. It's a proactive way of ensuring things are in the right place. So, the next time you're coding, let assertions be that nifty little alarm system, helping you build impeccable projects!
85. Customizing String Representations with __str__ and __repr__
Normal Approach: It’s like buying a pre-made model airplane. It’s functional and gets the job done, but it doesn’t have those personal touches that make it uniquely yours.
class Airplane:
def __init__(self, model, year):
self.model = model
self.year = year
plane = Airplane("Boeing 747", 1970)
print(plane) # Outputs: <__main__.Airplane object at 0x7f8cb194ed30>This default representation tells you about the object and its memory address, but not much else.
Pro Trick:
Now, imagine customizing that airplane with hand-painted details and unique colors. By overriding the __str__ and __repr__ methods, you can make the object representation more insightful, just like adding those personal touches.
class Airplane:
def __init__(self, model, year):
self.model = model
self.year = year
def __str__(self):
return f"{self.model} ({self.year})"
def __repr__(self):
return f"Airplane(model='{self.model}', year={self.year})"
plane = Airplane("Boeing 747", 1970)
print(plane) # Outputs: Boeing 747 (1970)
print(repr(plane)) # Outputs: Airplane(model='Boeing 747', year=1970)With this approach, the object’s representation is not just generic, but offers insights about the airplane model and year. So the next time you’re working on a class, remember to add those unique touches with __str__ and __repr__. Your debugging and logging sessions will surely thank you for it!
86. Magic Methods for Operator Overloading
Normal Approach: Let’s say you’re building a tower with basic Lego bricks. They fit together in a standard way. Similarly, by default, objects of custom classes don’t know how to handle operations like addition or multiplication.
class Box:
def __init__(self, content):
self.content = content
box1 = Box(5)
box2 = Box(10)
# box3 = box1 + box2 # This will raise an error. Python doesn't know how to add two Box objects.Trying to add two Box objects directly doesn’t work, just as trying to stick two Lego figures together head-to-head doesn’t quite fit.
Pro Trick: But what if you had special Lego pieces that allowed you to combine figures in creative ways? That’s the power of magic methods! By defining them in your class, you dictate how objects of that class should behave with standard operations.
class Box:
def __init__(self, content):
self.content = content
def __add__(self, other):
return Box(self.content + other.content)
def __mul__(self, multiplier):
return Box(self.content * multiplier)
box1 = Box(5)
box2 = Box(10)
box3 = box1 + box2 # Now it works!
box4 = box1 * 3 # And this too!
print(box3.content) # Outputs: 15
print(box4.content) # Outputs: 15With the magic methods in place, your objects can interact in custom ways, making them versatile and intuitive. Just like with those special Lego pieces, sometimes you need to think outside the box (pun intended) to create the perfect build. Happy coding and building!
87. Iterating with enumerate for Index and Value
Normal Approach: Imagine you’re flipping through a photo album. For each photo, you want to know its position in the album, but there are no page numbers. So, you end up counting the photos from the beginning every single time. That’s not efficient, is it?
In the coding world, this is similar to using range(len()) to iterate over a list to get both the index and the value.
fruits = ['apple', 'banana', 'cherry', 'date']
for i in range(len(fruits)):
print(f"Position {i} holds {fruits[i]}")It gets the job done, but it feels like there’s a more elegant solution waiting to be discovered.
Pro Trick:
Now, consider a version of that photo album, but with neat little numbers on every page. Finding a photo’s position becomes straightforward. In Python, this is akin to using enumerate. It gives you both the index and the value in one fell swoop!
for index, fruit in enumerate(fruits):
print(f"Position {index} holds {fruit}")It’s much cleaner and intuitive, right? Just as you’d prefer the numbered photo album, enumerate is the go-to choice for many Pythonistas. It's all about finding and using the tools that make life a tad bit easier and more enjoyable. Happy iterating!
88. Function Annotations for Better Documentation
Normal Approach: You know, when I first started coding, there wasn’t as much emphasis on readability. It’s a bit like having a cookbook without any ingredient labels. Sure, you can guess what the ingredients might be based on the method, but a little label would make things much clearer.
In Python, without annotations, you might see function documentation like this:
def add_numbers(a, b):
# a: int, b: int -> int
return a + bYou’ve got a comment that somewhat gives an idea about the expected types of the parameters and the return type. It works, but feels a bit like a makeshift solution.
Pro Trick: As coding evolved, so did the tools. Now, we have a neat way to make functions more descriptive — function annotations! It’s like our cookbook suddenly got those ingredient labels.
With function annotations, the same function becomes:
def add_numbers(a: int, b: int) -> int:
return a + bIt’s crystal clear what type of arguments the function expects and what it will return. Not only does this make the code more readable, but it also makes it more maintainable. Think of function annotations as a nifty labeling system that keeps things organized and easily understood. Happy cooking… I mean, coding!
89. Unicode and Byte String Distinctions
Normal Approach: Back in the early days of coding, handling text was like trying to enjoy an international film festival without subtitles. You might think you understand everything, but there’s always the risk of missing out on subtleties or even completely misinterpreting things.
For example, in Python, when you treat all strings in the same manner, you might get encoding surprises:
text = "café"
encoded_text = text.encode('utf-8')
print(encoded_text) # Outputs: b'caf\xc3\xa9'
decoded_text = encoded_text.decode('latin-1')
print(decoded_text) # Oops! This gives 'café', not what we expected!Without a careful understanding of what’s going on under the hood, you might stumble upon unexpected characters in your string.
Pro Trick: But just like how films have improved their subtitle technology, Python has evolved in how it manages strings. Understanding the difference between Unicode strings and byte strings can make your life way easier.
When working with text data:
- Use Unicode strings (
strin Python 3) for text processing. - Use byte strings (
bytesin Python 3) for binary data or when you're doing explicit encoding/decoding.
Here’s a more enlightened way:
text = "café"
encoded_text = text.encode('utf-8') # Properly encode to bytes using utf-8
print(encoded_text) # Outputs: b'caf\xc3\xa9'
decoded_text = encoded_text.decode('utf-8') # Decode using the same utf-8 encoding
print(decoded_text) # Voilà! We get 'café' as expected.Think of it like tuning into the right language channel for your movie. When you know which “language” (or encoding) to select, everything becomes clear, and there are no unexpected surprises. Enjoy the show!
90. Exploiting the __slots__ Directive
Have you ever bought a backpack with seemingly endless pockets? It can hold anything and everything you throw into it. But while the versatility is great, sometimes it can feel like a bit much, especially when you’re searching for that one tiny thing you need. Python classes are similar by default. When you create a class, it’s like a backpack with infinite pockets, allowing you to add new attributes on-the-fly.
class Backpack:
def __init__(self, item):
self.item = item
my_bag = Backpack("laptop")
my_bag.snack = "apple" # Dynamically adding a new attribute
print(my_bag.snack) # Outputs: appleWhile this flexibility is super, there’s a cost: memory overhead.
Pro Trick:
Now, imagine a sleek backpack with designated slots for specific items. It’s compact, lightweight, and efficient. That’s what the __slots__ directive offers for Python classes. By defining __slots__, you're specifying a fixed set of attributes, preventing the dynamic creation of new ones. This saves memory and streamlines attribute access.
class Backpack:
__slots__ = ['item'] # Only 'item' attribute is allowed
def __init__(self, item):
self.item = item
my_bag = Backpack("laptop")
# my_bag.snack = "apple" # Uncommenting this would raise an AttributeErrorWith __slots__, you've got a tailored space, optimized for exactly what you need and nothing more. It's like Marie Kondo-ing your Python objects. If it doesn't have a designated slot, it doesn't belong. Keep things tidy and efficient!
91. Dynamic Attribute Access with getattr, setattr, and delattr
Normal Approach: You know when you’re looking through a physical file cabinet, pulling out folders one by one, checking their labels, and occasionally, you find the drawer empty where a folder should be? That’s like directly accessing an object’s attributes in Python, where an error occurs if the attribute doesn’t exist.
class Document:
def __init__(self, title):
self.title = title
report = Document("Annual Report")
print(report.title) # Outputs: Annual Report
# print(report.author) # Uncommenting this would raise an AttributeErrorIt’s straightforward, but when the attribute doesn’t exist, you’ll hit an error, much like an unexpectedly empty drawer in the file cabinet.
Pro Trick:
Imagine now if the file cabinet could magically present to you the folder you’re thinking of or tell you it’s not there without you having to rummage around. The built-in functions getattr, setattr, and delattr provide this magic for Python objects, allowing dynamic and error-resistant attribute operations.
title = getattr(report, 'title', "Default Title") # Fetches attribute or returns a default
print(title) # Outputs: Annual Report
setattr(report, 'author', 'John Doe') # Dynamically set an attribute
print(report.author) # Outputs: John Doe
delattr(report, 'author') # Dynamically delete an attribute
# print(report.author) # Uncommenting now would raise an AttributeErrorWith these dynamic tools in your arsenal, you’re navigating the realm of Python objects with more agility, confidence, and fewer surprise errors. It’s like having a file cabinet assistant always ready to help out. Efficient and error-free, the way to go!
92. The Power of the Ellipsis ... for Placeholder
Normal Approach:
Imagine when you’re drafting a blueprint for a new house, and you know where the living room will be, but you haven’t yet figured out the design details. In Python, when sketching the structure of your code, you might use the pass statement or comments as placeholders, signaling areas to fill in later.
def calculate_area():
# TODO: implement this later
passIt’s like marking a space on your blueprint saying, “Living room goes here, details to come.”
Pro Trick:
However, what if you had a super concise and neat sticky note to slap onto that blueprint anytime you needed a placeholder? Enter the Ellipsis (...). It serves as a super concise placeholder, especially helpful in stubbing out functions, classes, or blocks of code.
def calculate_area():
...This tiny yet mighty ... is a wonderful reminder to come back and fill in the details. It's like those sticky notes, but cooler. When you revisit, it instantly catches your attention, nudging you, "Hey, remember to finish this up!"
93. Command-Line Argument Parsing with argparse
Normal Approach:
Let’s consider command-line arguments as the ingredients of a recipe. If you had to measure and prepare each ingredient manually every time without any instructions, it’d be a time-consuming ordeal. Traditionally, to handle command-line arguments in Python, one would directly manipulate sys.argv.
import sys
if len(sys.argv) > 1:
filename = sys.argv[1]
else:
print("Usage: python script.py <filename>")
sys.exit(1)
print(f"Processing file: {filename}")It works, but as your recipe (or script) becomes more intricate, juggling these ingredients can become tricky.
Pro Trick:
Now, picture a fantastic kitchen gadget that measures, categorizes, and prepares your ingredients based on a simple instruction card. That’s what argparse is like for command-line arguments.
import argparse
parser = argparse.ArgumentParser(description="Process a file.")
parser.add_argument("filename", help="The name of the file to process")
args = parser.parse_args()
print(f"Processing file: {args.filename}")By using argparse, you're essentially setting up a more structured and user-friendly way to handle those arguments. Plus, it automatically generates help and usage messages and issues errors when users provide invalid arguments. It's the kind of tool that streamlines the process, ensuring that you always get the recipe just right!
94. Context Variables with contextvars
Normal Approach: Imagine you’re hosting a dinner party, and you have a shared dish on the table. Every guest dips in their spoon, not knowing who took what and how much. That’s a bit like using global variables. They’re shared across functions, and keeping track of changes can be challenging, especially in multi-threaded scenarios.
global_var = "initial value"
def change_global():
global global_var
global_var = "changed value"
change_global()
print(global_var) # Outputs: changed valueThis can lead to unintended side effects if different parts of the code modify the global variable, especially if multiple threads or asynchronous tasks are involved.
Pro Trick:
Now, think of contextvars as giving each of your guests a separate, personalized bowl for the dish. Everyone has their own context, avoiding clashes. It's especially useful in scenarios like asynchronous programming where you want to preserve context across different parts of the code without unintended interference.
from contextvars import ContextVar
context_var = ContextVar('context_var', default="initial value")
def change_context():
token = context_var.set("changed value")
def print_context():
print(context_var.get())
change_context()
print_context() # Outputs: changed valueWith contextvars, each asynchronous task can have its own independent variable value, ensuring that contexts don't overlap and mix unintentionally. It's like giving everyone at the dinner table their own personal space, ensuring a harmonious dining experience!
95. Function Overloading with Single Dispatch
Normal Approach: Have you ever tried to speak multiple languages at the same time during a conversation? It’s like manually checking types in a function and then deciding what to do. It can be cumbersome, and the flow isn’t as smooth.
def area(shape, a, b=None):
if isinstance(shape, str):
if shape == "circle":
return 3.14 * a * a
elif shape == "rectangle" and b is not None:
return a * b
raise ValueError("Unsupported shape or incorrect parameters")
print(area("circle", 5)) # Outputs: 78.5
print(area("rectangle", 5, 4)) # Outputs: 20The above method works, but it becomes cluttered and less readable as more types and conditions are added.
Pro Trick:
Now, think of functools.singledispatch as having a translator who automatically detects which language you're speaking and responds accordingly. This allows for a more organized and clean way of handling multiple function versions based on type.
from functools import singledispatch
@singledispatch
def area(a):
raise ValueError("Unsupported shape or incorrect parameters")
@area.register(str)
def _(shape, a=None, b=None):
if shape == "circle":
return 3.14 * a * a
elif shape == "rectangle" and b is not None:
return a * b
print(area("circle", 5)) # Outputs: 78.5
print(area("rectangle", 5, 4)) # Outputs: 20By using singledispatch, the code is more modular and clearer. It's like having a fluent multilingual conversation without ever pausing to switch languages. Smooth and efficient!
96. Immutable Data Using bytes and bytearray
Normal Approach: Remember when we used to rewind cassette tapes with a pencil? It’s a bit like using regular strings for byte data. You can make it work, but it’s not really meant for that purpose.
data_str = "Hello, World!"
print(data_str[7]) # Outputs: W
# But trying to modify...
# data_str[7] = "V" # This will raise an errorThe string provides a way to store textual data, but it’s not built to handle byte-specific operations, and strings are immutable in Python.
Pro Trick:
Enter bytes and bytearray. It's like having a digital music player instead of the old cassette tapes. No need for pencils here!
data_bytes = bytes("Hello, World!", "utf-8")
print(data_bytes[7]) # Outputs: 87 (ASCII value of 'W')
# For mutable byte data:
data_bytearray = bytearray("Hello, World!", "utf-8")
data_bytearray[7] = ord('V')
print(data_bytearray) # Outputs: bytearray(b'Hello, Vorld!')By using bytes and bytearray, you're working with data in its most raw form, offering greater flexibility for byte-specific operations. Just like upgrading from cassettes to digital music, you get enhanced precision and ease of use. Dance on to those bytes!
97. Input Parsing with input and eval
Normal Approach: Do you remember when you’d buy a toy and it required assembly? But instead of instructions, you just got a picture of the final product? That’s like getting basic input as strings. You know what it should look like in the end, but there’s quite a journey to get there.
user_input = input("Enter a number: ")
# User enters: 5
number = int(user_input)
print(number + 10) # Outputs: 15In the above example, we’ve to explicitly convert the string input to an integer to perform arithmetic.
Pro Trick:
Now imagine you got a toy, and it magically assembled itself upon unboxing. That’s the charm of eval, but use it with caution. It's a bit like a magic toy: intriguing but can be unpredictable if misused.
user_input = input("Enter an expression: ")
# User enters: 5 + 10
result = eval(user_input)
print(result) # Outputs: 15By using eval, you can evaluate simple expressions directly. But remember, letting eval handle unchecked user input can be risky. It's a powerful tool that, if given the wrong instructions, can misbehave. So always ensure the user input is safe before evaluating. A bit of caution can save you from the unexpected!
98. The Zip-Longest Functionality with itertools.zip_longest
Normal Approach:
Imagine you’re at a dinner party, and there are two rows of chairs, one for you and your friends, and the other for the meals. If there are more friends than meals (or vice versa), someone will be left without. That’s how the regular zip function behaves - it pairs until one of the lists runs out.
names = ["Alice", "Bob", "Charlie"]
favorites = ["Pizza", "Burger"]
paired = list(zip(names, favorites))
print(paired)
# Outputs: [('Alice', 'Pizza'), ('Bob', 'Burger')]Poor Charlie doesn’t get his favorite food listed because there isn’t a corresponding element.
Pro Trick:
Now, think of itertools.zip_longest as the considerate host who notices the mismatch and offers an alternative option. It ensures that no one's left out.
from itertools import zip_longest
names = ["Alice", "Bob", "Charlie"]
favorites = ["Pizza", "Burger"]
paired = list(zip_longest(names, favorites, fillvalue="Unknown"))
print(paired)
# Outputs: [('Alice', 'Pizza'), ('Bob', 'Burger'), ('Charlie', 'Unknown')]With zip_longest, Charlie is still on the list, but with an 'Unknown' favorite. It's a more graceful way of handling mismatched pairings, ensuring everyone's included. It's like ensuring everyone at the party is taken care of, even if things don't align perfectly.
99. Redirecting Standard Output with io.StringIO
Normal Approach: Think of your computer console as a bulletin board in a community center. When you print something, it’s like pinning a note on that board. Everyone can see it immediately, but if you wanted to change or use that note elsewhere, it’s a bit tricky since it’s already out there.
print("Hello, World!")
# Outputs directly to the console: Hello, World!It works just fine if you want to quickly share a message. But what if you want to hold onto that note, maybe add some doodles or annotations before displaying it?
Pro Trick:
Enter io.StringIO. It's like having a personal notepad where you can write your messages, modify them, and then decide if you want to pin them on the community board or not.
import io
import sys
buffer = io.StringIO()
sys.stdout = buffer
print("Hello, World!")
# Now, the output is captured in the buffer, not displayed immediately.
message = buffer.getvalue()
print(f"Captured: {message}") # Outputs to the real console: Captured: Hello, World!\n
sys.stdout = sys.__stdout__ # Resetting the standard output back to the consoleWith this approach, you’ve effectively given yourself a space to prep your messages before sending them out into the world. It’s a bit like drafting a text before sending. Always handy when you need that extra layer of control.
100. Precision Handling with decimal
Normal Approach: Consider handling money in a digital wallet. If you’re relying on regular floating-point arithmetic, you might occasionally get unexpected results. It’s a bit like trying to count grains of sand on a windy beach — close, but never quite exact.
result = 0.1 + 0.1 + 0.1 - 0.3
print(result) # Might not be exactly 0.0 due to floating-point inaccuraciesWhile for many scenarios, these tiny errors might seem inconsequential, in cases like financial transactions, even minute discrepancies can become significant.
Pro Trick:
The decimal module is like trading your sandy, windy beach for the meticulous environment of a scientist's lab. It ensures that arithmetic is done with precision and accuracy, guarding against those sneaky rounding errors.
from decimal import Decimal
# Using Decimal for precise arithmetic
result = Decimal('0.1') + Decimal('0.1') + Decimal('0.1') - Decimal('0.3')
print(result) # Exactly 0.0With the decimal module, you can have confidence in your calculations, knowing they're being handled with the utmost precision. It's an essential tool for anyone looking to eliminate any guesswork in their arithmetic!
Congratulations on reaching the finish line! How did you feel about these tips and tricks? Were they helpful? If you’d like to see more from my extensive notebook, drop a comment, and I’d be thrilled to share more insights in future articles. Remember, a clap for this article or a follow goes a long way in supporting me.
To stay in the loop and foster community support, join my Discord group. Together, we’re unstoppable!
Thank you 👋❤️
In Plain English
Thank you for being a part of our community! Before you go:
- Be sure to clap and follow the writer! 👏
- You can find even more content at PlainEnglish.io 🚀
- Sign up for our free weekly newsletter. 🗞️
- Follow us on Twitter(X), LinkedIn, YouTube, and Discord.






