Python coding
53 subscribers
24 files
11 links
​A technical hub dedicated to Python programming, web development, and software engineering. Learn core syntax, build full-stack projects, and master backend development.
Download Telegram
Python coding
πŸš€ Python Tip #64: The typing.TypedDict Class The TypedDict class from Python's typing module lets you define dictionaries with specific keys and value types. It improves code readability and helps static type checkers detect errors. --- Importing TypedDict…
🐍 Python Tip #65: The typing.Protocol Class

The Protocol class from Python's typing module allows you to define a set of required methods and attributes without requiring inheritance.

This is known as structural typing or duck typing.

---

Importing Protocol

from typing import Protocol

---

Creating a Protocol

from typing import Protocol

class Speaker(Protocol):
def speak(self) -> str:
...

The `Speaker protocol requires a speak() method that returns a string.

---

Implementing the Protocol

``
python
from typing import Protocol

class Speaker(Protocol):
def speak(self) -> str:
...

class Person:
def speak(self) -> str:
return "Hello!"

person = Person()

print(person.speak())

Output:

python
Hello!

Notice that `Person` does **not** inherit from `Speaker`, but it still satisfies the protocol because it implements the required method.

---

Using Protocol in Functions

python
from typing import Protocol

class Animal(Protocol):
def sound(self) -> str:
...

class Dog:
def sound(self):
return "Woof!"

class Cat:
def sound(self):
return "Meow!"

def make_sound(animal: Animal):
print(animal.sound())

make_sound(Dog())
make_sound(Cat())

Output:

python
Woof!
Meow!

Both classes work because they provide the required `sound()` method.

---

Why use `Protocol`?

βœ“ Enables structural typing (duck typing)
βœ“ No need for explicit inheritance
βœ“ Makes code more flexible and reusable
βœ“ Commonly used in libraries, frameworks, APIs, and large Python projects

---

Challenge:

Create a protocol named:

python
Drawable

with a method:

python
draw()

Then:

1. Create two classes:

python
Circle
Rectangle
`

2. Implement the `draw()` method in both classes.
3. Write a function that accepts any `Drawable` object and calls its `draw()` method.
Python coding
🐍 Python Tip #65: The typing.Protocol Class The Protocol class from Python's typing module allows you to define a set of required methods and attributes without requiring inheritance. This is known as structural typing or duck typing. --- Importing Protocol…
πŸš€ Python Tip #66: The typing.Literal Type

The Literal type from Python's typing module allows you to restrict a variable or function parameter to specific, predefined values.

This improves type safety and makes your code easier to understand.

---

Importing Literal

from typing import Literal

---

Basic Example

from typing import Literal

Status = Literal["pending", "running", "completed"]

status: Status = "running"

print(status)

Output:

running

The variable can only have one of the specified values.

---

Using Literal in Functions

from typing import Literal

def set_theme(
theme: Literal["light", "dark"]
):
print(f"Theme: {theme}")

set_theme("dark")

Output:

Theme: dark

Passing any other value would be considered a type error by static type checkers.

---

Restricting Numeric Values

from typing import Literal

def roll_dice(
sides: Literal[4, 6, 8, 10, 12, 20]
):
print(f"Rolling a {sides}-sided dice.")

roll_dice(20)

Output:

Rolling a 20-sided dice.

---

Using Multiple Literal Values

from typing import Literal

Direction = Literal[
"north",
"south",
"east",
"west"
]

def move(direction: Direction):
print(f"Moving {direction}")

move("north")

Output:

Moving north

---

Why use Literal?

βœ“ Restricts values to predefined choices
βœ“ Improves code readability and maintainability
βœ“ Helps static type checkers detect invalid values
βœ“ Commonly used in APIs, configuration options, and application settings

---

Challenge:

Create a function named:

set_language()

The function should accept only these values:

"English"
"French"
"Spanish"

using `Literal.

Then call the function with:

``
python
"English"
`

and print the selected language.
Python coding
πŸš€ Python Tip #66: The typing.Literal Type The Literal type from Python's typing module allows you to restrict a variable or function parameter to specific, predefined values. This improves type safety and makes your code easier to understand. --- Importing…
πŸ’‘ Python Tip #67: The typing.Final Type

The Final type from Python's typing module indicates that a variable, constant, or method should not be reassigned or overridden.

While Python does not enforce this at runtime, static type checkers can detect violations.

---

Importing Final

from typing import Final

---

Declaring a Constant

from typing import Final

PI: Final = 3.14159

print(PI)

Output:

3.14159

The value of PI is intended to remain unchanged.

---

Using Final with Variables

from typing import Final

MAX_USERS: Final = 100

print(MAX_USERS)

Output:

100

Attempting to reassign it:

MAX_USERS = 200

A static type checker will report this as an error.

---

Using Final in Classes

from typing import Final

class Configuration:
VERSION: Final = "1.0.0"

print(Configuration.VERSION)

Output:

1.0.0

The `VERSION constant is intended to remain fixed.

---

Using
Final for Methods

``
python
from typing import Final

class Animal:
@Final
def speak(self):
print("Animal sound")

A subclass should not override the `speak()` method.

---

Why use `Final`?

βœ“ Clearly marks constants and fixed values
βœ“ Helps prevent accidental reassignment
βœ“ Improves code readability and maintainability
βœ“ Commonly used for configuration values, constants, and methods that should not be overridden

---

Challenge:

Create a program that:

1. Declares a constant named:

python
APP_NAME

using `Final`.

2. Assign it the value:

python
"Python Learning Hub"
`

3. Print the constant.

4. Try to assign a new value to it and observe the warning from your static type checker.
Python coding
πŸ’‘ Python Tip #67: The typing.Final Type The Final type from Python's typing module indicates that a variable, constant, or method should not be reassigned or overridden. While Python does not enforce this at runtime, static type checkers can detect violations.…
πŸš€ Python Tip #68: The typing.Annotated Type

The Annotated type from Python's typing module allows you to attach additional metadata to type hints.

This metadata can be used by frameworks, libraries, documentation tools, or static analysis tools without changing the underlying type.

---

Importing Annotated

from typing import Annotated

---

Basic Example

from typing import Annotated

age: Annotated[int, "Age must be between 0 and 120"] = 16

print(age)

Output:

16

The variable is still an int, but it now includes extra descriptive metadata.

---

Using Annotated in Functions

from typing import Annotated

def greet(
name: Annotated[str, "User's full name"]
):
print(f"Hello, {name}!")

greet("Yehya")

Output:

Hello, Yehya!

---

Using Multiple Metadata Values

You can attach more than one piece of metadata.

from typing import Annotated

temperature: Annotated[
float,
"Measured in Celsius",
"Range: -50 to 100"
] = 26.5

print(temperature)

Output:

26.5

---

Combining with Other Type Hints

from typing import Annotated

scores: list[
Annotated[int, "Exam score"]
] = [85, 92, 78]

print(scores)

Output:

[85, 92, 78]

The list contains integers, while the metadata describes what those integers represent.

---

Why use Annotated?

βœ“ Adds metadata without changing the original type
βœ“ Improves documentation and code readability
βœ“ Useful for validation frameworks and APIs
βœ“ Commonly used in FastAPI, Pydantic, and modern Python applications

---

Challenge:

Create a function named:

register_user()

The function should accept:

username
age

Use `Annotated to add the following descriptions:

- Username of the new user
- User's age in years

Then call the function with:

``
python
username = "Yehya"
age = 16
`
Python coding
πŸš€ Python Tip #68: The typing.Annotated Type The Annotated type from Python's typing module allows you to attach additional metadata to type hints. This metadata can be used by frameworks, libraries, documentation tools, or static analysis tools without changing…
🐍 Python Tip #69: The typing.NewType Function

The NewType function from Python's typing module creates a distinct type based on an existing type.

It helps prevent mixing different values that share the same underlying type, such as int or str.

---

Importing NewType

from typing import NewType

---

Creating a New Type

from typing import NewType

UserId = NewType("UserId", int)

user_id = UserId(101)

print(user_id)

Output:

101

Although UserId behaves like an int at runtime, static type checkers treat it as a separate type.

---

Using NewType in Functions

from typing import NewType

UserId = NewType("UserId", int)

def get_user(user_id: UserId):
print(f"User ID: {user_id}")

get_user(UserId(15))

Output:

User ID: 15

This makes the function's purpose clearer and helps prevent passing the wrong type.

---

Creating Multiple New Types

from typing import NewType

UserId = NewType("UserId", int)
ProductId = NewType("ProductId", int)

user = UserId(1)
product = ProductId(1)

print(user)
print(product)

Output:

1
1

Even though both are based on int, type checkers recognize them as different types.

---

Why use NewType?

βœ“ Creates meaningful custom types
βœ“ Prevents mixing logically different values
βœ“ Improves code readability and type safety
βœ“ Commonly used in APIs, databases, and large Python applications

---

Challenge:

Create two new types:

StudentId
TeacherId

Both should be based on int.

Then:

1. Create one value for each type.
2. Print both values.
3. Write a function that accepts only a StudentId and prints it.
`
Python coding
🐍 Python Tip #69: The typing.NewType Function The NewType function from Python's typing module creates a distinct type based on an existing type. It helps prevent mixing different values that share the same underlying type, such as int or str. --- Importing…
πŸš€ Python Tip #70: The operator Module

The operator module provides functions that correspond to Python's built-in operators.

It is especially useful when working with functions such as map(), filter(), and sorted().

---

Importing operator

import operator

---

Basic Example

import operator

print(operator.add(10, 5))
print(operator.sub(10, 5))
print(operator.mul(10, 5))
print(operator.truediv(10, 5))

Output:

15
5
50
2.0

These functions are equivalent to:

10 + 5
10 - 5
10 * 5
10 / 5

---

Using operator with map()

import operator

numbers = [1, 2, 3, 4]

result = list(map(operator.mul, numbers, [2, 2, 2, 2]))

print(result)

Output:

[2, 4, 6, 8]

---

Using operator.itemgetter()

itemgetter() is useful for sorting lists of dictionaries or tuples.

from operator import itemgetter

students = [
{"name": "Ali", "score": 85},
{"name": "Sara", "score": 95},
{"name": "John", "score": 78}
]

students.sort(key=itemgetter("score"))

print(students)

Output:

[
{'name': 'John', 'score': 78},
{'name': 'Ali', 'score': 85},
{'name': 'Sara', 'score': 95}
]

---

Sorting in Reverse

from operator import itemgetter

students.sort(
key=itemgetter("score"),
reverse=True
)

print(students)

Output:

[
{'name': 'Sara', 'score': 95},
{'name': 'Ali', 'score': 85},
{'name': 'John', 'score': 78}
]

---

Why use operator?

βœ“ Provides functions for common operators
βœ“ Works perfectly with functional programming tools
βœ“ Makes sorting complex data easier
βœ“ Commonly used with map(), sorted(), filter(), and data processing

---

Challenge:

Given:

students = [
{"name": "Ali", "score": 82},
{"name": "Sara", "score": 96},
{"name": "John", "score": 74},
{"name": "Mariam", "score": 89}
]

Use:

```python
operator.itemgetter()

to sort the students by their score from highest to lowest.

Expected order:

text
Sara
Mariam
Ali
John
`
Python coding
πŸš€ Python Tip #70: The operator Module The operator module provides functions that correspond to Python's built-in operators. It is especially useful when working with functions such as map(), filter(), and sorted(). --- Importing operator import operator…
🐍 Python Tip #71: The operator.attrgetter() Function

The attrgetter() function from Python's operator module lets you retrieve attributes from objects.

It is especially useful when sorting custom objects.

---

Importing attrgetter

from operator import attrgetter

---

Basic Example

from operator import attrgetter

class Student:
def __init__(self, name, score):
self.name = name
self.score = score

student = Student("Yehya", 95)

print(attrgetter("name")(student))
print(attrgetter("score")(student))

Output:

Yehya
95

---

Sorting Objects

from operator import attrgetter

class Student:
def __init__(self, name, score):
self.name = name
self.score = score

students = [
Student("Ali", 82),
Student("Sara", 96),
Student("John", 74),
Student("Mariam", 89)
]

students.sort(key=attrgetter("score"))

for student in students:
print(student.name, student.score)

Output:

John 74
Ali 82
Mariam 89
Sara 96

---

Sorting in Reverse

students.sort(
key=attrgetter("score"),
reverse=True
)

for student in students:
print(student.name, student.score)

Output:

Sara 96
Mariam 89
Ali 82
John 74

---

Getting Multiple Attributes

from operator import attrgetter

get_info = attrgetter("name", "score")

print(get_info(student))

Output:

('Yehya', 95)

---

Why use attrgetter()?

βœ“ Retrieves object attributes efficiently
βœ“ Makes sorting custom objects simple
βœ“ Works well with sorted() and .sort()
βœ“ Useful in object-oriented programming and data processing

---

Challenge:

Create a Product class with:

name
price

Create at least four products.

Then use:

attrgetter("price")

to sort them from cheapest to most expensive.
Python coding
🐍 Python Tip #71: The operator.attrgetter() Function The attrgetter() function from Python's operator module lets you retrieve attributes from objects. It is especially useful when sorting custom objects. --- Importing attrgetter from operator import…
πŸš€ Python Tip #72: The operator.methodcaller() Function

The methodcaller() function from Python's operator module creates a callable that calls a specific method on an object.

It is useful when working with functions like map().

---

Importing methodcaller

from operator import methodcaller

---

Basic Example

from operator import methodcaller

upper = methodcaller("upper")

print(upper("python"))

Output:

PYTHON

This is similar to:

"python".upper()

---

Using methodcaller() with map()

from operator import methodcaller

words = ["python", "java", "c++"]

result = list(map(methodcaller("upper"), words))

print(result)

Output:

['PYTHON', 'JAVA', 'C++']

---

Passing Arguments to a Method

methodcaller() can also pass arguments to the method.

from operator import methodcaller

words = ["python", "programming", "coding"]

result = list(
map(methodcaller("replace", "o", "0"), words)
)

print(result)

Output:

['pyth0n', 'pr0gramming', 'c0ding']

---

Using methodcaller() with Custom Classes

from operator import methodcaller

class Student:
def __init__(self, name):
self.name = name

def greet(self):
return f"Hello, {self.name}!"

students = [
Student("Ali"),
Student("Sara"),
Student("John")
]

greet = methodcaller("greet")

for message in map(greet, students):
print(message)

Output:

Hello, Ali!
Hello, Sara!
Hello, John!

---

Why use methodcaller()?

βœ“ Calls methods dynamically
βœ“ Works well with map()
βœ“ Can pass arguments to methods
βœ“ Useful in functional programming and data processing

---

Challenge:

Given:

words = [
"python",
"programming",
"developer"
]

Use:

```python
methodcaller()

with `map()` to convert every word to uppercase.

Expected Output:

python
['PYTHON', 'PROGRAMMING', 'DEVELOPER']
`
Python coding
πŸš€ Python Tip #72: The operator.methodcaller() Function The methodcaller() function from Python's operator module creates a callable that calls a specific method on an object. It is useful when working with functions like map(). --- Importing methodcaller…
🐍 Python Tip #73: The itertools Module

The itertools module provides powerful tools for working with iterators.

It can help you create combinations, permutations, repeated values, and efficient loops.

---

Importing itertools

import itertools

---

Creating Combinations

combinations() generates all possible combinations of a specific length.

from itertools import combinations

numbers = [1, 2, 3, 4]

result = list(combinations(numbers, 2))

print(result)

Output:

[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]

The order does not matter in combinations.

---

Creating Permutations

permutations() generates arrangements where order matters.

from itertools import permutations

numbers = [1, 2, 3]

result = list(permutations(numbers, 2))

print(result)

Output:

[
(1, 2),
(1, 3),
(2, 1),
(2, 3),
(3, 1),
(3, 2)
]

Notice that (1, 2) and (2, 1) are different.

---

Repeating Values with repeat()

from itertools import repeat

for value in repeat("Python", 3):
print(value)

Output:

Python
Python
Python

---

Why use itertools?

βœ“ Provides efficient iterator-based tools
βœ“ Useful for combinations and permutations
βœ“ Helps avoid writing complex loops manually
βœ“ Commonly used in algorithms, data processing, and mathematical problems

---

Challenge:

Given:

students = ["Ali", "Sara", "John", "Mariam"]

Use:

```python
combinations()

to generate every possible **pair of students**.

Expected Output:

python
('Ali', 'Sara')
('Ali', 'John')
('Ali', 'Mariam')
('Sara', 'John')
('Sara', 'Mariam')
('John', 'Mariam')
`
Python coding
🐍 Python Tip #73: The itertools Module The itertools module provides powerful tools for working with iterators. It can help you create combinations, permutations, repeated values, and efficient loops. --- Importing itertools import itertools --- Creating…
πŸš€ Python Tip #74: itertools.chain()

The chain() function from the itertools module lets you combine multiple iterables into one continuous iterator.

It's useful when you want to loop through several lists without creating a new combined list.

---

Importing chain

from itertools import chain

---

Basic Example

from itertools import chain

numbers1 = [1, 2, 3]
numbers2 = [4, 5, 6]

for number in chain(numbers1, numbers2):
print(number)

Output:

1
2
3
4
5
6

---

Combining Lists

You can also convert the result into a list:

from itertools import chain

list1 = ["A", "B"]
list2 = ["C", "D"]
list3 = ["E", "F"]

result = list(chain(list1, list2, list3))

print(result)

Output:

['A', 'B', 'C', 'D', 'E', 'F']

---

Why use chain()?

βœ“ Combines multiple iterables efficiently
βœ“ Avoids unnecessary nested loops
βœ“ Doesn't create a new combined list unless you explicitly convert it
βœ“ Useful for data processing and working with large collections

---

Challenge:

Given:

group1 = ["Ali", "Sara"]
group2 = ["John", "Mariam"]
group3 = ["Yusuf", "Aisha"]

Use:

```python
chain()

to loop through all six students and print their names one by one.

Expected Output:

text
Ali
Sara
John
Mariam
Yusuf
Aisha
`
Python coding
πŸš€ Python Tip #74: itertools.chain() The chain() function from the itertools module lets you combine multiple iterables into one continuous iterator. It's useful when you want to loop through several lists without creating a new combined list. --- Importing…
🐍 Python Tip #75: itertools.islice()

The islice() function from Python's itertools module allows you to select a specific portion of an iterable without creating the entire sequence in memory.

It's especially useful when working with large datasets, generators, and streams.

---

Importing islice

from itertools import islice

---

Basic Example

from itertools import islice

numbers = range(10)

result = list(islice(numbers, 5))

print(result)

Output:

[0, 1, 2, 3, 4]

This takes the first 5 items.

---

Using Start and Stop

from itertools import islice

numbers = range(10)

result = list(islice(numbers, 2, 7))

print(result)

Output:

[2, 3, 4, 5, 6]

The syntax is similar to:

numbers[2:7]

---

Using a Step

from itertools import islice

numbers = range(20)

result = list(islice(numbers, 0, 20, 3))

print(result)

Output:

[0, 3, 6, 9, 12, 15, 18]

The third argument specifies the step.

---

Working with a Generator

from itertools import islice

def numbers():
for i in range(1000000):
yield i

result = list(islice(numbers(), 10))

print(result)

Output:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Only the required values are consumed from the generator.

---

Why use islice()?

βœ“ Efficiently processes part of an iterable
βœ“ Doesn't require slicing support
βœ“ Works with generators and iterators
βœ“ Useful for large datasets and streams

---

Challenge:

Create a generator that produces numbers from 1 to 1000.

Use:

islice()

to extract only the numbers from position 50 through 59.

Expected Output:

[51, 52, 53, 54, 55, 56, 57, 58, 59, 60]
❀2
Python coding
🐍 Python Tip #75: itertools.islice() The islice() function from Python's itertools module allows you to select a specific portion of an iterable without creating the entire sequence in memory. It's especially useful when working with large datasets, generators…
🐍 Python Tip #76: Regular Expressions with re

Python's re module lets you search, match, extract, and replace text using regular expressions (regex).

Regex is extremely useful for text processing, validation, and searching patterns.

---

Importing re

import re

---

Searching for a Pattern

import re

text = "I am learning Python."

match = re.search(r"Python", text)

if match:
print("Python was found!")

Output:

Python was found!

---

Finding All Matches

Use findall() to get every occurrence of a pattern.

import re

text = "Python is powerful. Python is popular."

matches = re.findall(r"Python", text)

print(matches)

Output:

['Python', 'Python']

---

Extracting Numbers

import re

text = "I have 25 books and 3 notebooks."

numbers = re.findall(r"\d+", text)

print(numbers)

Output:

['25', '3']

Here, `\d+ means "one or more digits."

---

Replacing Text

Use
sub() to replace matching text.

``
python
import re

text = "Python is difficult."

result = re.sub(r"difficult", "fun", text)

print(result)

Output:

text
Python is fun.

---

Simple Email Pattern

python
import re

email = "student@example.com"

pattern = r"^[\w.-]+@[\w.-]+\.\w+$"

if re.fullmatch(pattern, email):
print("Valid email")
else:
print("Invalid email")

Output:

text
Valid email

---

Useful Regex Patterns

text
\d β†’ digit
\w β†’ letter, digit, or underscore
\s β†’ whitespace
+ β†’ one or more
* β†’ zero or more
? β†’ zero or one
^ β†’ beginning of string
$ β†’ end of string
. β†’ any character

---

Why use `re`?

βœ“ Search text using patterns
βœ“ Extract useful information
βœ“ Validate input formats
βœ“ Replace matching text
βœ“ Useful in automation, data processing, web development, and text analysis

---

Challenge:

Given:

python
text = "Contact us at 123-456-7890 or 987-654-3210."

Use `re.findall()` to extract all phone numbers.

Expected Output:

python
['123-456-7890', '987-654-3210']
`
Python coding
🐍 Python Tip #76: Regular Expressions with re Python's re module lets you search, match, extract, and replace text using regular expressions (regex). Regex is extremely useful for text processing, validation, and searching patterns. --- Importing re import…
🐍 Python Tip #77: yield from

The yield from statement lets a generator delegate part of its work to another iterable or generator.

It is especially useful when you want to combine generators without writing an extra loop.

---

Basic Example:

def numbers():
yield from [1, 2, 3, 4]

for number in numbers():
print(number)

Output:

1
2
3
4

This is equivalent to:

def numbers():
for number in [1, 2, 3, 4]:
yield number

---

Combining Generators:

def first():
yield 1
yield 2
yield 3

def second():
yield 4
yield 5
yield 6

def all_numbers():
yield from first()
yield from second()

print(list(all_numbers()))

Output:

[1, 2, 3, 4, 5, 6]

Instead of manually looping through each generator, yield from delegates the iteration automatically.

---

Using Multiple Iterables:

def combined():
yield from ["Python", "Java"]
yield from ["C++", "JavaScript"]

print(list(combined()))

Output:

['Python', 'Java', 'C++', 'JavaScript']

---

Why use yield from?

βœ“ Makes generator code shorter
βœ“ Combines multiple generators easily
βœ“ Avoids unnecessary nested loops
βœ“ Works efficiently with iterators
βœ“ Useful for data pipelines and recursive generators

---

Challenge:

Create three generators:

def fruits():
yield "Apple"
yield "Banana"

def vegetables():
yield "Carrot"
yield "Potato"

def foods():
# Use yield from here
pass

Make `foods() produce all four items.

Expected Output:

``
python
['Apple', 'Banana', 'Carrot', 'Potato']
`
Python coding
🐍 Python Tip #77: yield from The yield from statement lets a generator delegate part of its work to another iterable or generator. It is especially useful when you want to combine generators without writing an extra loop. --- Basic Example: def numbers():…
🐍 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

---

Basic Example

from itertools import groupby

numbers = [1, 1, 2, 2, 2, 3, 3]

for key, group in groupby(numbers):
print(key, list(group))

Output:

1 [1, 1]
2 [2, 2, 2]
3 [3, 3]

---

Important: groupby() Groups Consecutive Values

from itertools import groupby

numbers = [1, 2, 1, 2, 1]

for key, group in groupby(numbers):
print(key, list(group))

Output:

1 [1]
2 [2]
1 [1]
2 [2]
1 [1]

It does not automatically combine all identical values.

---

Grouping Objects by a Key

from itertools import groupby

students = [
{"name": "Ali", "grade": "A"},
{"name": "Sara", "grade": "A"},
{"name": "John", "grade": "B"},
{"name": "Mariam", "grade": "B"}
]

for grade, group in groupby(students, key=lambda student: student["grade"]):
print(grade, list(group))

Output:

A [{'name': 'Ali', 'grade': 'A'}, {'name': 'Sara', 'grade': 'A'}]
B [{'name': 'John', 'grade': 'B'}, {'name': 'Mariam', 'grade': 'B'}]

---

Sorting Before Grouping

If equal values are not next to each other, sort the data first.

from itertools import groupby

numbers = [2, 1, 2, 3, 1, 3, 2]

numbers.sort()

for key, group in groupby(numbers):
print(key, list(group))

Output:

1 [1, 1]
2 [2, 2, 2]
3 [3, 3]

---

Why use groupby()?

βœ“ Groups related consecutive data
βœ“ Works efficiently with iterators
βœ“ Useful for data processing and organization
βœ“ Can group complex objects using a key function

---

Challenge:

Given:

students = [
{"name": "Ali", "class": "A"},
{"name": "Sara", "class": "A"},
{"name": "John", "class": "B"},
{"name": "Mariam", "class": "B"},
{"name": "Yusuf", "class": "C"}
]

Use `groupby() to group the students by their "class"`.

Print each class and the students belonging to it.
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: 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 partial

from 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 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.

---

Importing weakref

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

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: 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 of a sequence.

---

Importing deque

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'])

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 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 bisect

import 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: 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 list every time.

---

Importing heapq

import 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: 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 much cleaner.

---

Importing defaultdict

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.

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}