Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on the bisect module! ๐ Ever wondered how to keep your lists sorted without the hassle of manually inserting elements in the right place? Thatโs exactly what weโll explore today!
Youโll discover how Pythonโs bisect
module can transform the way you work with sorted data. Whether youโre building leaderboards ๐, managing inventory systems ๐ฆ, or optimizing search algorithms ๐, understanding bisect is essential for writing efficient, elegant code.
By the end of this tutorial, youโll feel confident using bisect to maintain sorted lists like a pro! Letโs dive in! ๐โโ๏ธ
๐ Understanding Bisect
๐ค What is Bisect?
The bisect module is like a smart librarian ๐ who knows exactly where to place each new book on an already organized shelf. Think of it as your personal assistant that helps you insert items into sorted lists while keeping them perfectly ordered!
In Python terms, bisect
provides support for maintaining a list in sorted order without having to sort the list after each insertion. This means you can:
- โจ Insert elements at the correct position instantly
- ๐ Search sorted sequences blazingly fast
- ๐ก๏ธ Maintain order without expensive sorting operations
๐ก Why Use Bisect?
Hereโs why developers love bisect:
- Performance โก: O(log n) insertion vs O(n log n) for sort
- Simplicity ๐ฏ: Clean, readable code for sorted operations
- Memory Efficiency ๐พ: No need to create new sorted lists
- Real-time Updates ๐: Keep data sorted as it arrives
Real-world example: Imagine managing a game leaderboard ๐ฎ. With bisect, you can instantly insert new scores in the right position without re-sorting the entire list!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
import bisect
# ๐ Hello, bisect!
scores = [10, 30, 50, 70, 90] # ๐ฏ Already sorted list
new_score = 45
# ๐จ Find where to insert the new score
position = bisect.bisect_left(scores, new_score)
print(f"Insert {new_score} at position: {position}") # Position: 2
# โจ Insert the score
scores.insert(position, new_score)
print(f"Updated scores: {scores}") # [10, 30, 45, 50, 70, 90]
๐ก Explanation: Notice how bisect_left
finds the perfect spot for our new score! The list stays sorted without calling sort()
.
๐ฏ Common Patterns
Here are patterns youโll use daily:
import bisect
# ๐๏ธ Pattern 1: Using insort for direct insertion
numbers = [1, 3, 5, 7, 9]
bisect.insort(numbers, 4) # ๐ฏ Inserts 4 at the right position
print(numbers) # [1, 3, 4, 5, 7, 9]
# ๐จ Pattern 2: Finding insertion points
grades = [60, 70, 80, 90]
student_grade = 75
# ๐ bisect_left vs bisect_right
left_pos = bisect.bisect_left(grades, student_grade) # Returns 2
right_pos = bisect.bisect_right(grades, student_grade) # Returns 2
# ๐ Pattern 3: Searching in sorted lists
def grade_rank(score, breakpoints=[60, 70, 80, 90]):
# ๐ Returns grade level based on score
grades = ['F', 'D', 'C', 'B', 'A']
i = bisect.bisect(breakpoints, score)
return grades[i]
print(f"Score 85 gets grade: {grade_rank(85)} ๐") # Grade: B
๐ก Practical Examples
๐ Example 1: Smart Inventory System
Letโs build something real:
import bisect
from dataclasses import dataclass
from typing import List
# ๐๏ธ Define our product
@dataclass
class Product:
name: str
price: float
emoji: str
def __lt__(self, other):
# ๐ฐ Sort by price
return self.price < other.price
class SmartInventory:
def __init__(self):
self.products: List[Product] = []
# โ Add product maintaining price order
def add_product(self, product: Product):
bisect.insort(self.products, product)
print(f"Added {product.emoji} {product.name} at ${product.price}!")
# ๐ Find products in price range
def find_in_range(self, min_price: float, max_price: float):
# ๐ Find start and end positions
start = bisect.bisect_left(self.products,
Product("", min_price, ""))
end = bisect.bisect_right(self.products,
Product("", max_price, ""))
print(f"\n๐ Products between ${min_price}-${max_price}:")
for product in self.products[start:end]:
print(f" {product.emoji} {product.name}: ${product.price}")
# ๐ List all products
def show_inventory(self):
print("\n๐ฆ Current Inventory (sorted by price):")
for p in self.products:
print(f" {p.emoji} {p.name}: ${p.price}")
# ๐ฎ Let's use it!
inventory = SmartInventory()
inventory.add_product(Product("Laptop", 999.99, "๐ป"))
inventory.add_product(Product("Mouse", 29.99, "๐ฑ๏ธ"))
inventory.add_product(Product("Keyboard", 79.99, "โจ๏ธ"))
inventory.add_product(Product("Monitor", 299.99, "๐ฅ๏ธ"))
inventory.show_inventory()
inventory.find_in_range(50, 300) # Find mid-range products
๐ฏ Try it yourself: Add a method to find the cheapest n products or products nearest to a target price!
๐ฎ Example 2: Game Leaderboard System
Letโs make it fun:
import bisect
from datetime import datetime
from typing import List, Tuple
# ๐ High score entry
class ScoreEntry:
def __init__(self, player: str, score: int, emoji: str = "๐ฎ"):
self.player = player
self.score = score
self.emoji = emoji
self.timestamp = datetime.now()
def __lt__(self, other):
# ๐ Higher scores come first (reverse order)
return self.score > other.score
def __repr__(self):
return f"{self.emoji} {self.player}: {self.score}"
class GameLeaderboard:
def __init__(self, max_entries: int = 10):
self.scores: List[ScoreEntry] = []
self.max_entries = max_entries
# ๐ฏ Add new score
def add_score(self, player: str, score: int):
entry = ScoreEntry(player, score)
# ๐
Find position for new score
position = bisect.bisect_left(self.scores, entry)
# โจ Check if score makes the leaderboard
if position < self.max_entries:
self.scores.insert(position, entry)
print(f"๐ {player} earned {score} points! Rank: #{position + 1}")
# ๐ Keep only top scores
if len(self.scores) > self.max_entries:
removed = self.scores.pop()
print(f"๐ข {removed.player} dropped off the leaderboard")
else:
print(f"๐ช {player} scored {score}, but didn't make top {self.max_entries}")
# ๐ Display leaderboard
def show_leaderboard(self):
print("\n๐ LEADERBOARD ๐")
print("=" * 30)
for i, entry in enumerate(self.scores):
medal = ["๐ฅ", "๐ฅ", "๐ฅ"][i] if i < 3 else f"#{i+1}"
print(f"{medal} {entry}")
# ๐ Check if score would make leaderboard
def would_make_leaderboard(self, score: int) -> Tuple[bool, int]:
if not self.scores or len(self.scores) < self.max_entries:
return True, len(self.scores) + 1
test_entry = ScoreEntry("test", score)
position = bisect.bisect_left(self.scores, test_entry)
if position < self.max_entries:
return True, position + 1
return False, -1
# ๐ฎ Game time!
leaderboard = GameLeaderboard(max_entries=5)
# ๐ฏ Players submit scores
players_scores = [
("Alice", 1000, "๐ธ"),
("Bob", 850, "๐คด"),
("Charlie", 1200, "๐ง"),
("Diana", 950, "๐ฆธ"),
("Eve", 1100, "๐ฅท"),
("Frank", 800, "๐ค"),
("Grace", 1150, "๐")
]
for player, score, emoji in players_scores:
entry = ScoreEntry(player, score, emoji)
entry.emoji = emoji
leaderboard.add_score(player, score)
leaderboard.show_leaderboard()
# ๐ค Check if a score would make it
score_to_beat = 900
makes_it, rank = leaderboard.would_make_leaderboard(score_to_beat)
if makes_it:
print(f"\n๐ก A score of {score_to_beat} would rank #{rank}!")
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Custom Key Functions
When youโre ready to level up, try this advanced pattern:
import bisect
from functools import partial
# ๐ฏ Using key functions (Python 3.10+)
class Event:
def __init__(self, name: str, timestamp: float, emoji: str):
self.name = name
self.timestamp = timestamp
self.emoji = emoji
def __repr__(self):
return f"{self.emoji} {self.name} @ {self.timestamp}"
# ๐ช Custom comparison for bisect
events = [
Event("Login", 100.0, "๐"),
Event("Purchase", 150.0, "๐ณ"),
Event("Logout", 200.0, "๐ช")
]
# ๐ Find events after timestamp 125
timestamps = [e.timestamp for e in events]
index = bisect.bisect_left(timestamps, 125.0)
print(f"Events after 125.0: {events[index:]}")
# โจ Alternative: Using a key function wrapper
class KeyWrapper:
def __init__(self, iterable, key):
self.it = iterable
self.key = key
def __getitem__(self, i):
return self.key(self.it[i])
def __len__(self):
return len(self.it)
# ๐จ Use the wrapper
new_event = Event("Click", 175.0, "๐ฑ๏ธ")
wrapped = KeyWrapper(events, key=lambda e: e.timestamp)
position = bisect.bisect_left(wrapped, 175.0)
events.insert(position, new_event)
print(f"After insertion: {events}")
๐๏ธ Advanced Topic 2: Efficient Range Queries
For the brave developers:
import bisect
from typing import List, Tuple, Optional
class TimeSeriesData:
"""๐ Efficient time series with range queries"""
def __init__(self):
self.timestamps: List[float] = []
self.values: List[float] = []
self.emojis: List[str] = []
def add_point(self, timestamp: float, value: float, emoji: str = "๐"):
# ๐ฏ Insert maintaining time order
idx = bisect.bisect_left(self.timestamps, timestamp)
self.timestamps.insert(idx, timestamp)
self.values.insert(idx, value)
self.emojis.insert(idx, emoji)
def get_range(self, start: float, end: float) -> List[Tuple[float, float, str]]:
# ๐ Efficient range query using bisect
start_idx = bisect.bisect_left(self.timestamps, start)
end_idx = bisect.bisect_right(self.timestamps, end)
return list(zip(
self.timestamps[start_idx:end_idx],
self.values[start_idx:end_idx],
self.emojis[start_idx:end_idx]
))
def get_nearest(self, target: float) -> Optional[Tuple[float, float, str]]:
# ๐ฏ Find nearest timestamp
if not self.timestamps:
return None
idx = bisect.bisect_left(self.timestamps, target)
# ๐ Check neighbors
candidates = []
if idx > 0:
candidates.append(idx - 1)
if idx < len(self.timestamps):
candidates.append(idx)
# ๐ Find closest
if candidates:
closest_idx = min(candidates,
key=lambda i: abs(self.timestamps[i] - target))
return (self.timestamps[closest_idx],
self.values[closest_idx],
self.emojis[closest_idx])
return None
# ๐ฎ Usage example
series = TimeSeriesData()
# ๐ Add temperature readings
readings = [
(8.0, 15.2, "๐
"), # Morning
(12.0, 22.5, "โ๏ธ"), # Noon
(16.0, 20.1, "๐ค๏ธ"), # Afternoon
(20.0, 16.8, "๐"), # Evening
(10.0, 18.3, "โ
"), # Late morning
]
for time, temp, emoji in readings:
series.add_point(time, temp, emoji)
# ๐ Query ranges
print("๐ก๏ธ Temperatures between 10:00-16:00:")
for time, temp, emoji in series.get_range(10.0, 16.0):
print(f" {emoji} {time:.1f}:00 - {temp}ยฐC")
# ๐ฏ Find nearest reading
nearest = series.get_nearest(14.5)
if nearest:
time, temp, emoji = nearest
print(f"\n๐ฏ Nearest to 14:30 is {emoji} {time:.1f}:00 - {temp}ยฐC")
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Unsorted Lists
# โ Wrong way - bisect needs sorted lists!
numbers = [5, 2, 8, 1, 9] # ๐ฐ Not sorted!
bisect.insort(numbers, 6)
print(numbers) # [5, 2, 6, 8, 1, 9] - Wrong position!
# โ
Correct way - ensure list is sorted first!
numbers = [5, 2, 8, 1, 9]
numbers.sort() # ๐ก๏ธ Sort first!
bisect.insort(numbers, 6)
print(numbers) # [1, 2, 5, 6, 8, 9] - Perfect!
๐คฏ Pitfall 2: bisect_left vs bisect_right
# โ Confusion with duplicates
scores = [10, 20, 20, 20, 30]
# ๐ค Where does 20 go?
left = bisect.bisect_left(scores, 20) # Returns 1 (before all 20s)
right = bisect.bisect_right(scores, 20) # Returns 4 (after all 20s)
print(f"bisect_left: {left}, bisect_right: {right}")
# โ
Choose based on your needs!
# Use bisect_left for: "insert before duplicates"
# Use bisect_right for: "insert after duplicates"
# ๐ก Example: Stable sorting (preserving order)
data = [(20, "first"), (20, "second"), (20, "third")]
values = [item[0] for item in data]
# ๐ฏ To maintain order, use bisect_right
new_item = (20, "fourth")
pos = bisect.bisect_right(values, new_item[0])
print(f"Insert 'fourth' at position {pos} to maintain order")
๐ Pitfall 3: Performance with Large Lists
# โ Inefficient - using insert() on large lists
import time
large_list = list(range(0, 1000000, 2)) # Even numbers
# ๐ฐ Slow insertion
start = time.time()
pos = bisect.bisect_left(large_list, 500001)
large_list.insert(pos, 500001) # O(n) operation!
print(f"Insert took: {time.time() - start:.4f} seconds")
# โ
Better approach - use deque for frequent insertions
from collections import deque
# ๐ Or consider alternative data structures
# - heapq for priority queues
# - SortedList from sortedcontainers
# - Binary search trees for complex operations
๐ ๏ธ Best Practices
- ๐ฏ Always Verify Sorting: Check that your list is sorted before using bisect
- ๐ Choose the Right Function:
bisect_left
for unique insertions,bisect_right
for stability - ๐ก๏ธ Handle Edge Cases: Empty lists, single elements, duplicates
- ๐จ Use Type Hints: Make your code self-documenting
- โจ Consider Alternatives: For frequent modifications, consider
sortedcontainers
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Movie Recommendation System
Create a movie recommendation system using bisect:
๐ Requirements:
- โ Movies with ratings (1-10) maintained in sorted order
- ๐ท๏ธ Find movies within a rating range
- ๐ค Track user watch history with timestamps
- ๐ Find movies watched in a time period
- ๐จ Each movie needs a genre emoji!
๐ Bonus Points:
- Add personalized recommendations based on rating patterns
- Implement โsimilar moviesโ feature
- Create a โtrending nowโ list with time decay
๐ก Solution
๐ Click to see solution
import bisect
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List, Tuple, Optional
import random
# ๐ฌ Movie recommendation system!
@dataclass
class Movie:
title: str
rating: float
genre: str
emoji: str
def __lt__(self, other):
return self.rating < other.rating
@dataclass
class WatchEntry:
movie_title: str
timestamp: datetime
user_rating: float
def __lt__(self, other):
return self.timestamp < other.timestamp
class MovieRecommender:
def __init__(self):
self.movies: List[Movie] = []
self.watch_history: List[WatchEntry] = []
self.genre_emojis = {
"Action": "๐ฅ", "Comedy": "๐", "Drama": "๐ญ",
"Horror": "๐ป", "Sci-Fi": "๐", "Romance": "๐"
}
# โ Add movie to catalog
def add_movie(self, title: str, rating: float, genre: str):
emoji = self.genre_emojis.get(genre, "๐ฌ")
movie = Movie(title, rating, genre, emoji)
bisect.insort(self.movies, movie)
print(f"โ
Added: {emoji} {title} (โ
{rating})")
# ๐ Find movies in rating range
def find_by_rating(self, min_rating: float, max_rating: float) -> List[Movie]:
start = bisect.bisect_left(self.movies, Movie("", min_rating, "", ""))
end = bisect.bisect_right(self.movies, Movie("", max_rating, "", ""))
return self.movies[start:end]
# ๐ค Add to watch history
def watch_movie(self, title: str, user_rating: float):
entry = WatchEntry(title, datetime.now(), user_rating)
bisect.insort(self.watch_history, entry)
print(f"๐๏ธ Watched: {title} - You rated it โ
{user_rating}")
# ๐
Get movies watched in time period
def get_watch_history(self, hours_ago: int) -> List[WatchEntry]:
cutoff = datetime.now() - timedelta(hours=hours_ago)
dummy = WatchEntry("", cutoff, 0)
start = bisect.bisect_left(self.watch_history, dummy)
return self.watch_history[start:]
# ๐ฏ Get personalized recommendations
def get_recommendations(self, count: int = 5) -> List[Movie]:
if not self.watch_history:
# ๐ New user - recommend top rated
return self.movies[-count:][::-1]
# ๐ Calculate average user rating
avg_rating = sum(w.user_rating for w in self.watch_history) / len(self.watch_history)
# ๐ฏ Find movies near user's average preference
target = avg_rating + 0.5 # Slightly above their average
recommendations = []
# ๐ Search around target rating
idx = bisect.bisect_left(self.movies, Movie("", target, "", ""))
# ๐ Get movies from both directions
left, right = idx - 1, idx
while len(recommendations) < count and (left >= 0 or right < len(self.movies)):
if right < len(self.movies):
movie = self.movies[right]
if movie.title not in [w.movie_title for w in self.watch_history]:
recommendations.append(movie)
right += 1
if left >= 0 and len(recommendations) < count:
movie = self.movies[left]
if movie.title not in [w.movie_title for w in self.watch_history]:
recommendations.append(movie)
left -= 1
return recommendations
# ๐ Show stats
def show_stats(self):
print("\n๐ Movie Stats:")
print(f" ๐ฌ Total movies: {len(self.movies)}")
print(f" ๐๏ธ Movies watched: {len(self.watch_history)}")
if self.movies:
avg = sum(m.rating for m in self.movies) / len(self.movies)
print(f" โญ Average rating: {avg:.1f}")
# ๐ฎ Test it out!
recommender = MovieRecommender()
# ๐ฌ Add movies
movies_data = [
("The Matrix", 8.7, "Sci-Fi"),
("Inception", 8.8, "Sci-Fi"),
("The Godfather", 9.2, "Drama"),
("Pulp Fiction", 8.9, "Drama"),
("The Dark Knight", 9.0, "Action"),
("Forrest Gump", 8.8, "Drama"),
("Interstellar", 8.6, "Sci-Fi"),
("The Shawshank Redemption", 9.3, "Drama"),
("Fight Club", 8.8, "Drama"),
("Avengers: Endgame", 8.4, "Action")
]
for title, rating, genre in movies_data:
recommender.add_movie(title, rating, genre)
# ๐ค Simulate watching
recommender.watch_movie("The Matrix", 9.0)
recommender.watch_movie("Inception", 8.5)
# ๐ฏ Get recommendations
print("\n๐ฏ Recommended for you:")
for movie in recommender.get_recommendations(3):
print(f" {movie.emoji} {movie.title} (โ
{movie.rating})")
# ๐ Find highly rated movies
print("\n๐ Highly rated movies (8.8+):")
for movie in recommender.find_by_rating(8.8, 10.0):
print(f" {movie.emoji} {movie.title} (โ
{movie.rating})")
recommender.show_stats()
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Use bisect to maintain sorted lists efficiently ๐ช
- โ Choose between bisect_left and bisect_right appropriately ๐ก๏ธ
- โ Build efficient search and insertion algorithms ๐ฏ
- โ Implement real-world systems with sorted data ๐
- โ Optimize performance for large datasets! ๐
Remember: bisect is your friend when working with sorted data! Itโs fast, efficient, and makes your code cleaner. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered the bisect module!
Hereโs what to do next:
- ๐ป Practice with the movie recommendation exercise
- ๐๏ธ Use bisect in your next project that needs sorted data
- ๐ Explore the
heapq
module for priority queues - ๐ Check out
sortedcontainers
for more advanced sorted collections
Remember: Every Python expert started where you are now. Keep coding, keep learning, and most importantly, have fun with sorted data! ๐
Happy coding! ๐๐โจ