Python coding
π Python Tip #78: itertools.groupby() The groupby() function from Python's itertools module groups consecutive elements that have the same key. It's useful for organizing and analyzing sequences of data. --- Importing groupby from itertools import groupbyβ¦
π Python Tip #79:
The
This is useful when you want to reuse a function with certain values fixed.
---
Importing
---
Basic Example
Output:
Here,
So:
is effectively:
---
Creating Multiple Specialized Functions
Output:
---
Using
Output:
---
Why use
β Reuses existing functions
β Pre-fills function arguments
β Creates specialized versions of functions
β Useful with callbacks,
---
Challenge:
Create a function:
Then use
where the tax is fixed at:
Test it with:
Expected output:
functools.partial()The
partial() function from Python's functools module lets you create a new function with some arguments already filled in.This is useful when you want to reuse a function with certain values fixed.
---
Importing
partialfrom functools import partial
---
Basic Example
from functools import partial
def multiply(a, b):
return a * b
double = partial(multiply, 2)
print(double(5))
print(double(10))
Output:
10
20
Here,
2 is automatically supplied as the first argument.So:
double(5)
is effectively:
multiply(2, 5)
---
Creating Multiple Specialized Functions
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5))
print(cube(5))
Output:
25
125
---
Using
partial() with map()from functools import partial
def add(number, amount):
return number + amount
add_10 = partial(add, amount=10)
numbers = [1, 2, 3, 4]
result = list(map(add_10, numbers))
print(result)
Output:
[11, 12, 13, 14]
---
Why use
partial()?β Reuses existing functions
β Pre-fills function arguments
β Creates specialized versions of functions
β Useful with callbacks,
map(), event handlers, and APIs---
Challenge:
Create a function:
def calculate(price, tax):
return price + (price * tax)
Then use
partial() to create:calculate_15_percent
where the tax is fixed at:
0.15
Test it with:
100
Expected output:
115.0
Python coding
π Python Tip #79: functools.partial() The partial() function from Python's functools module lets you create a new function with some arguments already filled in. This is useful when you want to reuse a function with certain values fixed. --- Importingβ¦
π Python Tip #80: The
Python's
This can be useful when building caches, object registries, and memory-sensitive applications.
---
Importing
import weakref
---
Basic Example
import weakref
class User:
def init(self, name):
self.name = name
user = User("Yehya")
reference = weakref.ref(user)
print(reference().name)
Output:
Yehya
---
What Happens When the Object Is Deleted?
import weakref
class User:
def init(self, name):
self.name = name
user = User("Yehya")
reference = weakref.ref(user)
print(reference())
del user
print(reference())
Output:
<main.User object at ...>
None
After
The weak reference doesn't keep it alive.
---
Using a Weak Reference Callback
You can run a function when the referenced object is about to be finalized.
import weakref
class User:
pass
def object_deleted(reference):
print("Object was deleted.")
user = User()
reference = weakref.ref(
user,
object_deleted
)
del user
Possible output:
Object was deleted.
---
Why use
β Helps avoid unnecessary memory retention
β Useful for caches and object registries
β Allows objects to be garbage-collected normally
β Useful in advanced Python applications and frameworks
---
Challenge:
Create a class named:
Product
Then:
1. Create a
2. Create a weak reference to it.
3. Print the object through the weak reference.
4. Delete the original object.
5. Check the weak reference again.
What do you expect it to return after the object is deleted?
weakref ModulePython's
weakref module lets you create references to objects without preventing those objects from being garbage-collected.This can be useful when building caches, object registries, and memory-sensitive applications.
---
Importing
weakrefimport weakref
---
Basic Example
import weakref
class User:
def init(self, name):
self.name = name
user = User("Yehya")
reference = weakref.ref(user)
print(reference().name)
Output:
Yehya
reference() gives you access to the original object while it still exists.---
What Happens When the Object Is Deleted?
import weakref
class User:
def init(self, name):
self.name = name
user = User("Yehya")
reference = weakref.ref(user)
print(reference())
del user
print(reference())
Output:
<main.User object at ...>
None
After
user is deleted and no strong references remain, the object can be garbage-collected.The weak reference doesn't keep it alive.
---
Using a Weak Reference Callback
You can run a function when the referenced object is about to be finalized.
import weakref
class User:
pass
def object_deleted(reference):
print("Object was deleted.")
user = User()
reference = weakref.ref(
user,
object_deleted
)
del user
Possible output:
Object was deleted.
---
Why use
weakref?β Helps avoid unnecessary memory retention
β Useful for caches and object registries
β Allows objects to be garbage-collected normally
β Useful in advanced Python applications and frameworks
---
Challenge:
Create a class named:
Product
Then:
1. Create a
Product object.2. Create a weak reference to it.
3. Print the object through the weak reference.
4. Delete the original object.
5. Check the weak reference again.
What do you expect it to return after the object is deleted?
Python coding
π Python Tip #80: The weakref Module Python's weakref module lets you create references to objects without preventing those objects from being garbage-collected. This can be useful when building caches, object registries, and memory-sensitive applications.β¦
π Python Tip #81:
Ever needed a list where you're constantly adding and removing items from both ends?
That's where
---
Importing
from collections import deque
---
Basic Example
from collections import deque
queue = deque(["Ali", "Sara", "John"])
queue.append("Mariam")
queue.appendleft("Yusuf")
print(queue)
Output:
deque(['Yusuf', 'Ali', 'Sara', 'John', 'Mariam'])
---
Removing Items
from collections import deque
queue = deque(["Ali", "Sara", "John"])
first = queue.popleft()
last = queue.pop()
print(first)
print(last)
print(queue)
Output:
Ali
John
deque(['Sara'])
---
Why not just use a normal list?
You can use:
numbers.pop(0)
But removing the first item from a large list can be slower because the remaining elements have to be shifted.
With
numbers.popleft()
Removing from the left is designed to be efficient.
---
Useful Methods
append(x) β add to the right
appendleft(x) β add to the left
pop() β remove from the right
popleft() β remove from the left
extend(items) β add multiple items to the right
rotate(n) β rotate the elements
---
Real Example: A Simple Queue
from collections import deque
tasks = deque()
tasks.append("Download file")
tasks.append("Process data")
tasks.append("Send report")
while tasks:
task = tasks.popleft()
print("Processing:", task)
Output:
Processing: Download file
Processing: Process data
Processing: Send report
This is a simple example of a FIFO queue:
First In β First Out
---
Why use
β Fast adding/removing from both ends
β Great for queues and task processing
β Useful for sliding windows and buffers
β Better suited than lists for frequent left-side operations
β Part of Python's standard library
---
Challenge:
Create a
["A", "B", "C"]
Then:
1. Add
2. Add
3. Remove the first item.
4. Remove the last item.
5. Print the final deque.
What do you get?
collections.dequeEver needed a list where you're constantly adding and removing items from both ends?
That's where
deque comes in.deque means "double-ended queue" and is designed for fast operations at both the beginning and the end of a sequence.---
Importing
dequefrom collections import deque
---
Basic Example
from collections import deque
queue = deque(["Ali", "Sara", "John"])
queue.append("Mariam")
queue.appendleft("Yusuf")
print(queue)
Output:
deque(['Yusuf', 'Ali', 'Sara', 'John', 'Mariam'])
---
Removing Items
from collections import deque
queue = deque(["Ali", "Sara", "John"])
first = queue.popleft()
last = queue.pop()
print(first)
print(last)
print(queue)
Output:
Ali
John
deque(['Sara'])
popleft() removes from the left side.pop() removes from the right side.---
Why not just use a normal list?
You can use:
numbers.pop(0)
But removing the first item from a large list can be slower because the remaining elements have to be shifted.
With
deque:numbers.popleft()
Removing from the left is designed to be efficient.
---
Useful Methods
append(x) β add to the right
appendleft(x) β add to the left
pop() β remove from the right
popleft() β remove from the left
extend(items) β add multiple items to the right
rotate(n) β rotate the elements
---
Real Example: A Simple Queue
from collections import deque
tasks = deque()
tasks.append("Download file")
tasks.append("Process data")
tasks.append("Send report")
while tasks:
task = tasks.popleft()
print("Processing:", task)
Output:
Processing: Download file
Processing: Process data
Processing: Send report
This is a simple example of a FIFO queue:
First In β First Out
---
Why use
deque?β Fast adding/removing from both ends
β Great for queues and task processing
β Useful for sliding windows and buffers
β Better suited than lists for frequent left-side operations
β Part of Python's standard library
---
Challenge:
Create a
deque containing:["A", "B", "C"]
Then:
1. Add
"D" to the right.2. Add
"Z" to the left.3. Remove the first item.
4. Remove the last item.
5. Print the final deque.
What do you get?
Python coding
π Python Tip #81: collections.deque Ever needed a list where you're constantly adding and removing items from both ends? That's where deque comes in. deque means "double-ended queue" and is designed for fast operations at both the beginning and the endβ¦
π Python Tip #82: The
Ever wanted to keep a list sorted without manually searching for where a new value should go?
Python's
It uses binary search to find the correct position in an already-sorted list.
---
Importing
import bisect
---
Finding Where to Insert
import bisect
numbers = [10, 20, 30, 40]
position = bisect.bisect(numbers, 25)
print(position)
Output:
2
The result
---
Inserting Automatically
You can use
import bisect
numbers = [10, 20, 30, 40]
bisect.insort(numbers, 25)
print(numbers)
Output:
[10, 20, 25, 30, 40]
No need to manually search for the correct position.
---
Another Example
import bisect
scores = [45, 60, 72, 85, 95]
bisect.insort(scores, 80)
print(scores)
Output:
[45, 60, 72, 80, 85, 95]
The list stays sorted automatically.
---
When duplicate values exist, these functions help decide where to insert.
import bisect
numbers = [10, 20, 20, 20, 30]
print(bisect.bisect_left(numbers, 20))
print(bisect.bisect_right(numbers, 20))
Output:
1
4
---
Why use
β Keeps sorted lists organized
β Uses binary search to find positions
β Useful for ranking and score systems
β Great for maintaining ordered data
β Built into Python's standard library
---
Challenge:
Start with:
numbers = [10, 20, 30, 40, 50]
Use
25
35
45
Then print the final list.
Expected result:
[10, 20, 25, 30, 35, 40, 45, 50]
bisect ModuleEver wanted to keep a list sorted without manually searching for where a new value should go?
Python's
bisect module can help.It uses binary search to find the correct position in an already-sorted list.
---
Importing
bisectimport bisect
---
Finding Where to Insert
import bisect
numbers = [10, 20, 30, 40]
position = bisect.bisect(numbers, 25)
print(position)
Output:
2
The result
2 means 25 should be inserted at index 2.---
Inserting Automatically
You can use
insort() to find the position AND insert the value.import bisect
numbers = [10, 20, 30, 40]
bisect.insort(numbers, 25)
print(numbers)
Output:
[10, 20, 25, 30, 40]
No need to manually search for the correct position.
---
Another Example
import bisect
scores = [45, 60, 72, 85, 95]
bisect.insort(scores, 80)
print(scores)
Output:
[45, 60, 72, 80, 85, 95]
The list stays sorted automatically.
---
bisect_left() vs bisect_right()When duplicate values exist, these functions help decide where to insert.
import bisect
numbers = [10, 20, 20, 20, 30]
print(bisect.bisect_left(numbers, 20))
print(bisect.bisect_right(numbers, 20))
Output:
1
4
bisect_left() gives the position before the existing 20s.bisect_right() gives the position after them.---
Why use
bisect?β Keeps sorted lists organized
β Uses binary search to find positions
β Useful for ranking and score systems
β Great for maintaining ordered data
β Built into Python's standard library
---
Challenge:
Start with:
numbers = [10, 20, 30, 40, 50]
Use
bisect.insort() to insert:25
35
45
Then print the final list.
Expected result:
[10, 20, 25, 30, 35, 40, 45, 50]
Python coding
π Python Tip #82: The bisect Module Ever wanted to keep a list sorted without manually searching for where a new value should go? Python's bisect module can help. It uses binary search to find the correct position in an already-sorted list. --- Importingβ¦
π Python Tip #83:
Need to quickly find the smallest or largest items in a collection?
Python's
It's especially useful when you repeatedly need the smallest or largest values without sorting the entire list every time.
---
Importing
import heapq
---
Creating a Min-Heap
A normal list can be turned into a heap using
import heapq
numbers = [7, 2, 9, 1, 5]
heapq.heapify(numbers)
print(numbers)
The smallest value is always at index
print(numbers[0])
Output:
1
---
Removing the Smallest Item
Use
import heapq
numbers = [7, 2, 9, 1, 5]
heapq.heapify(numbers)
smallest = heapq.heappop(numbers)
print(smallest)
print(numbers)
Output:
1
[2, 5, 9, 7]
The heap structure isn't necessarily completely sorted.
It only guarantees that the smallest item is at the front.
---
Adding an Item
Use
import heapq
numbers = [2, 5, 7]
heapq.heapify(numbers)
heapq.heappush(numbers, 1)
print(numbers)
Output:
[1, 2, 7, 5]
---
Getting the Smallest Items
You can use
import heapq
numbers = [15, 3, 27, 8, 12, 1]
result = heapq.nsmallest(3, numbers)
print(result)
Output:
[1, 3, 8]
You can also use
result = heapq.nlargest(3, numbers)
print(result)
Output:
[27, 15, 12]
---
Real Example: Priority Tasks
import heapq
tasks = []
heapq.heappush(tasks, (2, "Send email"))
heapq.heappush(tasks, (1, "Fix critical bug"))
heapq.heappush(tasks, (3, "Update documentation"))
while tasks:
priority, task = heapq.heappop(tasks)
print(priority, task)
Output:
1 Fix critical bug
2 Send email
3 Update documentation
The smaller priority number gets processed first.
---
Why use
β Useful for priority queues
β Efficiently handles the smallest items
β Can find the largest/smallest N values
β Useful in scheduling and algorithms
β Common in real-world data processing
---
Challenge:
Given:
numbers = [42, 7, 19, 3, 25, 11]
Use
1. Turn the list into a heap.
2. Remove the smallest value.
3. Add
4. Print the smallest 3 values.
Try solving it without using
heapqNeed to quickly find the smallest or largest items in a collection?
Python's
heapq module gives you a heap-based priority queue.It's especially useful when you repeatedly need the smallest or largest values without sorting the entire list every time.
---
Importing
heapqimport heapq
---
Creating a Min-Heap
A normal list can be turned into a heap using
heapify().import heapq
numbers = [7, 2, 9, 1, 5]
heapq.heapify(numbers)
print(numbers)
The smallest value is always at index
0.print(numbers[0])
Output:
1
---
Removing the Smallest Item
Use
heappop():import heapq
numbers = [7, 2, 9, 1, 5]
heapq.heapify(numbers)
smallest = heapq.heappop(numbers)
print(smallest)
print(numbers)
Output:
1
[2, 5, 9, 7]
The heap structure isn't necessarily completely sorted.
It only guarantees that the smallest item is at the front.
---
Adding an Item
Use
heappush():import heapq
numbers = [2, 5, 7]
heapq.heapify(numbers)
heapq.heappush(numbers, 1)
print(numbers)
Output:
[1, 2, 7, 5]
---
Getting the Smallest Items
You can use
nsmallest() when you only need a few of the smallest values.import heapq
numbers = [15, 3, 27, 8, 12, 1]
result = heapq.nsmallest(3, numbers)
print(result)
Output:
[1, 3, 8]
You can also use
nlargest():result = heapq.nlargest(3, numbers)
print(result)
Output:
[27, 15, 12]
---
Real Example: Priority Tasks
import heapq
tasks = []
heapq.heappush(tasks, (2, "Send email"))
heapq.heappush(tasks, (1, "Fix critical bug"))
heapq.heappush(tasks, (3, "Update documentation"))
while tasks:
priority, task = heapq.heappop(tasks)
print(priority, task)
Output:
1 Fix critical bug
2 Send email
3 Update documentation
The smaller priority number gets processed first.
---
Why use
heapq?β Useful for priority queues
β Efficiently handles the smallest items
β Can find the largest/smallest N values
β Useful in scheduling and algorithms
β Common in real-world data processing
---
Challenge:
Given:
numbers = [42, 7, 19, 3, 25, 11]
Use
heapq to:1. Turn the list into a heap.
2. Remove the smallest value.
3. Add
5.4. Print the smallest 3 values.
Try solving it without using
sorted().
Python coding
π Python Tip #83: heapq Need to quickly find the smallest or largest items in a collection? Python's heapq module gives you a heap-based priority queue. It's especially useful when you repeatedly need the smallest or largest values without sorting the entireβ¦
π Python Tip #84:
Ever needed a dictionary where missing keys should automatically get a default value?
That's exactly what
It's part of Python's
---
Importing
from collections import defaultdict
---
Basic Example
from collections import defaultdict
scores = defaultdict(int)
scores["Ali"] += 10
scores["Sara"] += 20
print(scores)
Output:
defaultdict(<class 'int'>, {'Ali': 10, 'Sara': 20})
With a normal dictionary, trying to modify a key that doesn't exist would require creating it first.
---
Counting Items
One useful application is counting occurrences:
from collections import defaultdict
fruits = ["apple", "banana", "apple", "orange", "banana", "apple"]
counts = defaultdict(int)
for fruit in fruits:
counts[fruit] += 1
print(dict(counts))
Output:
{'apple': 3, 'banana': 2, 'orange': 1}
---
Creating Lists Automatically
You can also use
from collections import defaultdict
students = defaultdict(list)
students["Class A"].append("Ali")
students["Class A"].append("Sara")
students["Class B"].append("John")
print(dict(students))
Output:
{'Class A': ['Ali', 'Sara'], 'Class B': ['John']}
You don't need to manually create an empty list for every new class.
---
Another Example: Grouping Data
from collections import defaultdict
products = [
("Laptop", "Electronics"),
("Phone", "Electronics"),
("Desk", "Furniture"),
("Chair", "Furniture")
]
categories = defaultdict(list)
for product, category in products:
categories[category].append(product)
print(dict(categories))
Output:
{
'Electronics': ['Laptop', 'Phone'],
'Furniture': ['Desk', 'Chair']
}
---
Why use
β Automatically handles missing keys
β Great for counting
β Useful for grouping data
β Makes dictionary-based code cleaner
β Part of Python's standard library
---
Challenge:
Given:
words = ["python", "java", "python", "c", "java", "python"]
Use
Expected result:
{'python': 3, 'java': 2, 'c': 1}
collections.defaultdictEver needed a dictionary where missing keys should automatically get a default value?
That's exactly what
defaultdict is useful for.It's part of Python's
collections module and can make grouping and counting data much cleaner.---
Importing
defaultdictfrom collections import defaultdict
---
Basic Example
from collections import defaultdict
scores = defaultdict(int)
scores["Ali"] += 10
scores["Sara"] += 20
print(scores)
Output:
defaultdict(<class 'int'>, {'Ali': 10, 'Sara': 20})
With a normal dictionary, trying to modify a key that doesn't exist would require creating it first.
defaultdict(int) automatically starts missing values at 0.---
Counting Items
One useful application is counting occurrences:
from collections import defaultdict
fruits = ["apple", "banana", "apple", "orange", "banana", "apple"]
counts = defaultdict(int)
for fruit in fruits:
counts[fruit] += 1
print(dict(counts))
Output:
{'apple': 3, 'banana': 2, 'orange': 1}
---
Creating Lists Automatically
You can also use
list as the default factory.from collections import defaultdict
students = defaultdict(list)
students["Class A"].append("Ali")
students["Class A"].append("Sara")
students["Class B"].append("John")
print(dict(students))
Output:
{'Class A': ['Ali', 'Sara'], 'Class B': ['John']}
You don't need to manually create an empty list for every new class.
---
Another Example: Grouping Data
from collections import defaultdict
products = [
("Laptop", "Electronics"),
("Phone", "Electronics"),
("Desk", "Furniture"),
("Chair", "Furniture")
]
categories = defaultdict(list)
for product, category in products:
categories[category].append(product)
print(dict(categories))
Output:
{
'Electronics': ['Laptop', 'Phone'],
'Furniture': ['Desk', 'Chair']
}
---
Why use
defaultdict?β Automatically handles missing keys
β Great for counting
β Useful for grouping data
β Makes dictionary-based code cleaner
β Part of Python's standard library
---
Challenge:
Given:
words = ["python", "java", "python", "c", "java", "python"]
Use
defaultdict(int) to count how many times each programming language appears.Expected result:
{'python': 3, 'java': 2, 'c': 1}
Python coding
π Python Tip #84: collections.defaultdict Ever needed a dictionary where missing keys should automatically get a default value? That's exactly what defaultdict is useful for. It's part of Python's collections module and can make grouping and counting dataβ¦
π Python Tip #85:
Sometimes you have a list of values and want to repeatedly combine them into one final result.
That's where
It applies a function to the first two items, then uses the result with the next item, and keeps going until only one final value remains.
---
Importing
from functools import reduce
---
Basic Example
from functools import reduce
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda a, b: a + b, numbers)
print(total)
Output:
15
What's happening?
1 + 2 β 3
3 + 3 β 6
6 + 4 β 10
10 + 5 β 15
---
Multiplying Values
from functools import reduce
numbers = [2, 3, 4]
result = reduce(lambda a, b: a * b, numbers)
print(result)
Output:
24
Because:
2 Γ 3 Γ 4 = 24
---
Using a Normal Function
You don't have to use a lambda.
from functools import reduce
def add(a, b):
return a + b
numbers = [10, 20, 30, 40]
result = reduce(add, numbers)
print(result)
Output:
100
---
Using an Initial Value
You can provide a starting value as the third argument.
from functools import reduce
numbers = [1, 2, 3]
result = reduce(
lambda a, b: a + b,
numbers,
10
)
print(result)
Output:
16
The calculation starts with:
10 + 1 + 2 + 3 = 16
---
When Should You Use
It's useful when a sequence needs to be repeatedly combined into one value.
Examples include:
β Calculating products
β Combining values
β Building custom aggregation logic
β Functional programming
β Processing a sequence into one final result
For simple sums, though,
---
Challenge:
Given:
numbers = [2, 4, 6, 8]
Use
Expected output:
384
Can you do it without using
functools.reduce()Sometimes you have a list of values and want to repeatedly combine them into one final result.
That's where
reduce() comes in.It applies a function to the first two items, then uses the result with the next item, and keeps going until only one final value remains.
---
Importing
reducefrom functools import reduce
---
Basic Example
from functools import reduce
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda a, b: a + b, numbers)
print(total)
Output:
15
What's happening?
1 + 2 β 3
3 + 3 β 6
6 + 4 β 10
10 + 5 β 15
---
Multiplying Values
from functools import reduce
numbers = [2, 3, 4]
result = reduce(lambda a, b: a * b, numbers)
print(result)
Output:
24
Because:
2 Γ 3 Γ 4 = 24
---
Using a Normal Function
You don't have to use a lambda.
from functools import reduce
def add(a, b):
return a + b
numbers = [10, 20, 30, 40]
result = reduce(add, numbers)
print(result)
Output:
100
---
Using an Initial Value
You can provide a starting value as the third argument.
from functools import reduce
numbers = [1, 2, 3]
result = reduce(
lambda a, b: a + b,
numbers,
10
)
print(result)
Output:
16
The calculation starts with:
10 + 1 + 2 + 3 = 16
---
When Should You Use
reduce()?It's useful when a sequence needs to be repeatedly combined into one value.
Examples include:
β Calculating products
β Combining values
β Building custom aggregation logic
β Functional programming
β Processing a sequence into one final result
For simple sums, though,
sum() is usually clearer.---
Challenge:
Given:
numbers = [2, 4, 6, 8]
Use
reduce() to multiply all the numbers together.Expected output:
384
Can you do it without using
math.prod()?
Python coding
π Python Tip #85: functools.reduce() Sometimes you have a list of values and want to repeatedly combine them into one final result. That's where reduce() comes in. It applies a function to the first two items, then uses the result with the next item, andβ¦
π Python Tip #86:
When you create decorators in Python, there's a small problem that can easily be overlooked: the decorator can replace the original function's name and documentation.
---
Importing
from functools import wraps
---
The Problem Without
def my_decorator(func):
def wrapper():
print("Before function")
func()
return wrapper
@my_decorator
def greet():
"""Say hello to the user."""
print("Hello!")
print(greet.name)
print(greet.doc)
Output:
wrapper
None
The original function's metadata has been replaced by the wrapper's metadata.
---
Using
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper():
print("Before function")
func()
return wrapper
@my_decorator
def greet():
"""Say hello to the user."""
print("Hello!")
print(greet.name)
print(greet.doc)
Output:
greet
Say hello to the user.
Now Python knows that the wrapped function is still
---
Why Is This Useful?
Decorators are common in real Python projects.
They're used for things like:
β Logging
β Authentication
β Timing functions
β Caching
β Validation
β Permission checks
β Framework features
Using
---
A More Practical Example
from functools import wraps
import time
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.name} took {end - start:.6f} seconds")
return result
return wrapper
@timer
def calculate():
return sum(range(100000))
print(calculate())
The decorator measures the function's execution time while
---
Quick Rule
When writing your own decorators, a good habit is:
@wraps(func)
Put it directly above the wrapper function.
---
Challenge:
Create a decorator called
1. Prints
2. Runs the original function.
3. Prints
4. Uses
Then decorate a function called
It should still print:
hello
functools.wraps()When you create decorators in Python, there's a small problem that can easily be overlooked: the decorator can replace the original function's name and documentation.
functools.wraps() fixes that by preserving important information about the original function.---
Importing
wrapsfrom functools import wraps
---
The Problem Without
wraps()def my_decorator(func):
def wrapper():
print("Before function")
func()
return wrapper
@my_decorator
def greet():
"""Say hello to the user."""
print("Hello!")
print(greet.name)
print(greet.doc)
Output:
wrapper
None
The original function's metadata has been replaced by the wrapper's metadata.
---
Using
wraps()from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper():
print("Before function")
func()
return wrapper
@my_decorator
def greet():
"""Say hello to the user."""
print("Hello!")
print(greet.name)
print(greet.doc)
Output:
greet
Say hello to the user.
Now Python knows that the wrapped function is still
greet.---
Why Is This Useful?
Decorators are common in real Python projects.
They're used for things like:
β Logging
β Authentication
β Timing functions
β Caching
β Validation
β Permission checks
β Framework features
Using
@wraps(func) makes your decorators behave more like the original functions.---
A More Practical Example
from functools import wraps
import time
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.name} took {end - start:.6f} seconds")
return result
return wrapper
@timer
def calculate():
return sum(range(100000))
print(calculate())
The decorator measures the function's execution time while
@wraps() preserves the original function's metadata.---
Quick Rule
When writing your own decorators, a good habit is:
@wraps(func)
Put it directly above the wrapper function.
---
Challenge:
Create a decorator called
logger that:1. Prints
"Function started" before running a function.2. Runs the original function.
3. Prints
"Function finished" afterward.4. Uses
@wraps(func).Then decorate a function called
hello() and check its __name__.It should still print:
hello
Python coding
π Python Tip #86: functools.wraps() When you create decorators in Python, there's a small problem that can easily be overlooked: the decorator can replace the original function's name and documentation. functools.wraps() fixes that by preserving importantβ¦
π Python Tip #87:
Ever needed to work with multiple dictionaries as if they were one?
---
Importing
from collections import ChainMap
---
Basic Example
from collections import ChainMap
defaults = {
"theme": "light",
"language": "English"
}
user_settings = {
"theme": "dark"
}
settings = ChainMap(user_settings, defaults)
print(settings["theme"])
print(settings["language"])
Output:
dark
English
Python checks the dictionaries from left to right.
So if a key exists in
---
Why Is This Useful?
Imagine an application has:
1. User settings
2. Project settings
3. Default settings
Instead of copying everything into one dictionary, you can use:
settings = ChainMap(
user_settings,
project_settings,
defaults
)
If the user has a custom value, it takes priority.
If not, Python checks the next dictionary.
---
Adding a New Dictionary
You can add another mapping with
from collections import ChainMap
defaults = {"color": "blue"}
user = {"name": "Ali"}
settings = ChainMap(user, defaults)
temporary = settings.new_child(
{"color": "red"}
)
print(temporary["color"])
Output:
red
The new dictionary gets the highest priority.
---
Updating Values
If you change a value:
settings["name"] = "Sara"
the change is made in the first dictionary in the chain.
It keeps references to the original mappings.
---
Real-World Example
from collections import ChainMap
system_config = {
"debug": False,
"language": "English"
}
app_config = {
"debug": True
}
config = ChainMap(app_config, system_config)
print(config["debug"])
print(config["language"])
Output:
True
English
This pattern can be useful for configuration systems where different levels of settings override each other.
---
Why use
β Combines multiple dictionaries without copying them
β Useful for configuration systems
β Supports priority-based lookups
β Keeps the original dictionaries separate
β Useful when working with layered settings
---
Challenge:
Create three dictionaries:
defaults = {"theme": "light", "font": "Arial"}
user = {"theme": "dark"}
temporary = {"font": "Courier"}
Use
1.
2.
3.
Then print the values of:
theme
font
Expected output:
dark
Courier
collections.ChainMapEver needed to work with multiple dictionaries as if they were one?
ChainMap from the collections module lets you combine multiple dictionaries into a single view without actually merging or copying them.---
Importing
ChainMapfrom collections import ChainMap
---
Basic Example
from collections import ChainMap
defaults = {
"theme": "light",
"language": "English"
}
user_settings = {
"theme": "dark"
}
settings = ChainMap(user_settings, defaults)
print(settings["theme"])
print(settings["language"])
Output:
dark
English
Python checks the dictionaries from left to right.
So if a key exists in
user_settings, that value is used first.---
Why Is This Useful?
Imagine an application has:
1. User settings
2. Project settings
3. Default settings
Instead of copying everything into one dictionary, you can use:
settings = ChainMap(
user_settings,
project_settings,
defaults
)
If the user has a custom value, it takes priority.
If not, Python checks the next dictionary.
---
Adding a New Dictionary
You can add another mapping with
new_child():from collections import ChainMap
defaults = {"color": "blue"}
user = {"name": "Ali"}
settings = ChainMap(user, defaults)
temporary = settings.new_child(
{"color": "red"}
)
print(temporary["color"])
Output:
red
The new dictionary gets the highest priority.
---
Updating Values
If you change a value:
settings["name"] = "Sara"
the change is made in the first dictionary in the chain.
ChainMap doesn't merge everything into one new dictionary.It keeps references to the original mappings.
---
Real-World Example
from collections import ChainMap
system_config = {
"debug": False,
"language": "English"
}
app_config = {
"debug": True
}
config = ChainMap(app_config, system_config)
print(config["debug"])
print(config["language"])
Output:
True
English
This pattern can be useful for configuration systems where different levels of settings override each other.
---
Why use
ChainMap?β Combines multiple dictionaries without copying them
β Useful for configuration systems
β Supports priority-based lookups
β Keeps the original dictionaries separate
β Useful when working with layered settings
---
Challenge:
Create three dictionaries:
defaults = {"theme": "light", "font": "Arial"}
user = {"theme": "dark"}
temporary = {"font": "Courier"}
Use
ChainMap so that:1.
temporary has the highest priority.2.
user comes next.3.
defaults is the fallback.Then print the values of:
theme
font
Expected output:
dark
Courier
Python coding
π Python Tip #87: collections.ChainMap Ever needed to work with multiple dictionaries as if they were one? ChainMap from the collections module lets you combine multiple dictionaries into a single view without actually merging or copying them. --- Importingβ¦
π Python Tip #88:
Ever needed a variable whose value is local to the current task or execution context?
Python's
---
Importing
import contextvars
---
Creating a Context Variable
import contextvars
user = contextvars.ContextVar("user")
user.set("Yehya")
print(user.get())
Output:
Yehya
---
Using a Default Value
import contextvars
language = contextvars.ContextVar(
"language",
default="English"
)
print(language.get())
Output:
English
If you haven't assigned a value yet, the default is returned.
---
Changing the Value
import contextvars
username = contextvars.ContextVar(
"username",
default="Guest"
)
print(username.get())
username.set("Yehya")
print(username.get())
Output:
Guest
Yehya
---
Using
import contextvars
current_user = contextvars.ContextVar(
"current_user",
default="Guest"
)
def show_user():
print("Current user:", current_user.get())
current_user.set("Ali")
show_user()
current_user.set("Sara")
show_user()
Output:
Current user: Ali
Current user: Sara
---
Why Is This Useful?
β Async applications
β Web servers
β Request-specific data
β Logging context
β User/session information
β Code where multiple tasks need separate values
For example, a web application might store a request ID in a
---
Important Difference
A normal global variable is shared:
current_user = "Ali"
A
This becomes especially useful with
---
Challenge:
Create a
current_user
Give it a default value of:
"Guest"
Then:
1. Print the default value.
2. Change it to
3. Create a function that prints the current user.
4. Call the function.
Expected output:
Guest
Yehya
contextvarsEver needed a variable whose value is local to the current task or execution context?
Python's
contextvars module is especially useful in asynchronous applications where multiple tasks can be running at the same time.---
Importing
contextvarsimport contextvars
---
Creating a Context Variable
import contextvars
user = contextvars.ContextVar("user")
user.set("Yehya")
print(user.get())
Output:
Yehya
ContextVar stores a value that belongs to the current execution context.---
Using a Default Value
import contextvars
language = contextvars.ContextVar(
"language",
default="English"
)
print(language.get())
Output:
English
If you haven't assigned a value yet, the default is returned.
---
Changing the Value
import contextvars
username = contextvars.ContextVar(
"username",
default="Guest"
)
print(username.get())
username.set("Yehya")
print(username.get())
Output:
Guest
Yehya
---
Using
ContextVar in Functionsimport contextvars
current_user = contextvars.ContextVar(
"current_user",
default="Guest"
)
def show_user():
print("Current user:", current_user.get())
current_user.set("Ali")
show_user()
current_user.set("Sara")
show_user()
Output:
Current user: Ali
Current user: Sara
---
Why Is This Useful?
ContextVar is commonly useful in:β Async applications
β Web servers
β Request-specific data
β Logging context
β User/session information
β Code where multiple tasks need separate values
For example, a web application might store a request ID in a
ContextVar so different parts of the application can access it without passing the ID through every function.---
Important Difference
A normal global variable is shared:
current_user = "Ali"
A
ContextVar is designed so different execution contexts can have different values.This becomes especially useful with
asyncio and other concurrent code.---
Challenge:
Create a
ContextVar called:current_user
Give it a default value of:
"Guest"
Then:
1. Print the default value.
2. Change it to
"Yehya".3. Create a function that prints the current user.
4. Call the function.
Expected output:
Guest
Yehya
Python coding
π Python Tip #88: contextvars Ever needed a variable whose value is local to the current task or execution context? Python's contextvars module is especially useful in asynchronous applications where multiple tasks can be running at the same time. --- β¦
π Python Tip #89:
Sometimes you have a few related values and want to access them with dot notation instead of dictionary brackets.
---
Importing
from types import SimpleNamespace
---
Basic Example
from types import SimpleNamespace
user = SimpleNamespace(
name="Yehya",
age=16,
language="Python"
)
print(user.name)
print(user.age)
print(user.language)
Output:
Yehya
16
Python
Instead of:
user["name"]
you can use:
user.name
---
Changing Values
The attributes can be changed normally:
user.age = 17
print(user.age)
Output:
17
You can also add new attributes:
user.country = "Ethiopia"
print(user.country)
Output:
Ethiopia
---
Creating One From a Dictionary
You can also convert dictionary data into a namespace:
from types import SimpleNamespace
data = {
"name": "Sara",
"score": 95
}
student = SimpleNamespace(**data)
print(student.name)
print(student.score)
Output:
Sara
95
---
Converting Back to a Dictionary
Use
data = vars(student)
print(data)
Output:
{'name': 'Sara', 'score': 95}
---
When Is It Useful?
β Small data containers
β Configuration objects
β Temporary structured data
β Making dictionary data easier to access
β Quick prototypes
It's not a replacement for
---
Real Example
from types import SimpleNamespace
server = SimpleNamespace(
host="localhost",
port=8000,
debug=True
)
print(f"Server: {server.host}:{server.port}")
print("Debug:", server.debug)
Output:
Server: localhost:8000
Debug: True
---
Challenge:
Create a
name = "Yehya"
grade = 11
subject = "Python"
Then:
1. Print the student's name.
2. Change the grade to 12.
3. Add a new attribute called
4. Print all the information.
Can you do it without using a dictionary?
types.SimpleNamespaceSometimes you have a few related values and want to access them with dot notation instead of dictionary brackets.
SimpleNamespace from Python's types module gives you a simple object for storing attributes.---
Importing
SimpleNamespacefrom types import SimpleNamespace
---
Basic Example
from types import SimpleNamespace
user = SimpleNamespace(
name="Yehya",
age=16,
language="Python"
)
print(user.name)
print(user.age)
print(user.language)
Output:
Yehya
16
Python
Instead of:
user["name"]
you can use:
user.name
---
Changing Values
The attributes can be changed normally:
user.age = 17
print(user.age)
Output:
17
You can also add new attributes:
user.country = "Ethiopia"
print(user.country)
Output:
Ethiopia
---
Creating One From a Dictionary
You can also convert dictionary data into a namespace:
from types import SimpleNamespace
data = {
"name": "Sara",
"score": 95
}
student = SimpleNamespace(**data)
print(student.name)
print(student.score)
Output:
Sara
95
---
Converting Back to a Dictionary
Use
vars():data = vars(student)
print(data)
Output:
{'name': 'Sara', 'score': 95}
---
When Is It Useful?
SimpleNamespace can be handy for:β Small data containers
β Configuration objects
β Temporary structured data
β Making dictionary data easier to access
β Quick prototypes
It's not a replacement for
dataclass when you need a more structured data model.---
Real Example
from types import SimpleNamespace
server = SimpleNamespace(
host="localhost",
port=8000,
debug=True
)
print(f"Server: {server.host}:{server.port}")
print("Debug:", server.debug)
Output:
Server: localhost:8000
Debug: True
---
Challenge:
Create a
SimpleNamespace called student with:name = "Yehya"
grade = 11
subject = "Python"
Then:
1. Print the student's name.
2. Change the grade to 12.
3. Add a new attribute called
score.4. Print all the information.
Can you do it without using a dictionary?
Python coding
π Python Tip #89: types.SimpleNamespace Sometimes you have a few related values and want to access them with dot notation instead of dictionary brackets. SimpleNamespace from Python's types module gives you a simple object for storing attributes. --- Importingβ¦
π Python Tip #90:
If a function keeps receiving the same arguments and doing the same expensive calculation again and again, caching can save a lot of work.
Python's
---
Importing
from functools import cache
---
Basic Example
from functools import cache
@cache
def square(number):
print("Calculating...")
return number ** 2
print(square(5))
print(square(5))
print(square(10))
Output:
Calculating...
25
25
Calculating...
100
Notice that
The second
---
A More Useful Example
from functools import cache
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10))
Output:
55
Without caching, recursive Fibonacci calculations repeat many of the same calculations.
With
---
Checking the Cache
You can inspect the cache with:
fibonacci.cache_info()
For example, you might see:
CacheInfo(hits=8, misses=11, maxsize=None, currsize=11)
---
Clearing the Cache
If you want to remove all stored results:
fibonacci.cache_clear()
After that, the function will calculate the results again when called.
---
Important Note
That means stored results can continue growing as new arguments are used.
If you need a limited cache size,
---
Why use
β Avoids repeating expensive calculations
β Extremely useful with recursive functions
β Simple to add with
β Can make repeated function calls much faster
β Part of Python's standard library
---
Challenge:
Create a cached function:
@cache
def factorial(n):
It should calculate:
5! = 120
Then call:
factorial(5)
factorial(5)
factorial(6)
Finally, use:
factorial.cache_info()
and check how many cache hits you get.
functools.cache()If a function keeps receiving the same arguments and doing the same expensive calculation again and again, caching can save a lot of work.
Python's
functools.cache() automatically remembers previous results and reuses them when the same arguments appear again.---
Importing
cachefrom functools import cache
---
Basic Example
from functools import cache
@cache
def square(number):
print("Calculating...")
return number ** 2
print(square(5))
print(square(5))
print(square(10))
Output:
Calculating...
25
25
Calculating...
100
Notice that
"Calculating..." only appears once for 5.The second
square(5) gets the stored result instead of running the function again.---
A More Useful Example
from functools import cache
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10))
Output:
55
Without caching, recursive Fibonacci calculations repeat many of the same calculations.
With
@cache, previously calculated results are reused.---
Checking the Cache
You can inspect the cache with:
fibonacci.cache_info()
For example, you might see:
CacheInfo(hits=8, misses=11, maxsize=None, currsize=11)
hits = results retrieved from the cache.misses = calculations that had to actually run.currsize = number of stored results.---
Clearing the Cache
If you want to remove all stored results:
fibonacci.cache_clear()
After that, the function will calculate the results again when called.
---
Important Note
@cache is basically an unlimited cache.That means stored results can continue growing as new arguments are used.
If you need a limited cache size,
functools.lru_cache() is usually more appropriate.---
Why use
cache()?β Avoids repeating expensive calculations
β Extremely useful with recursive functions
β Simple to add with
@cacheβ Can make repeated function calls much faster
β Part of Python's standard library
---
Challenge:
Create a cached function:
@cache
def factorial(n):
It should calculate:
5! = 120
Then call:
factorial(5)
factorial(5)
factorial(6)
Finally, use:
factorial.cache_info()
and check how many cache hits you get.