+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 146 of 343

๐Ÿš€ __call__: Making Objects Callable Like Functions

Master Python's __call__ method to create callable objects with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿ’ŽAdvanced
25 min read

Prerequisites

  • Basic understanding of Python classes and objects ๐Ÿ“
  • Python installation (3.8+) ๐Ÿ
  • Understanding of magic methods ๐Ÿ’ป

What you'll learn

  • Understand how __call__ makes objects callable ๐ŸŽฏ
  • Apply callable objects in real projects ๐Ÿ—๏ธ
  • Debug common callable object issues ๐Ÿ›
  • Write clean, Pythonic code using __call__ โœจ

๐ŸŽฏ Introduction

Welcome to the magical world of callable objects in Python! ๐ŸŽ‰ Have you ever wished you could use an object like a function? Thatโ€™s exactly what the __call__ method lets you do!

In this tutorial, weโ€™ll explore how to make your objects callable, turning them into powerful, stateful functions. Whether youโ€™re building decorators ๐ŸŽจ, creating function factories ๐Ÿญ, or implementing complex algorithms, understanding __call__ will open up new possibilities in your Python journey!

By the end of this tutorial, youโ€™ll be creating objects that behave like functions but with superpowers! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Callable Objects

๐Ÿค” What Makes an Object Callable?

A callable object is like a Swiss Army knife ๐Ÿ”ง - it looks like a regular object but can be โ€œcalledโ€ like a function! Think of it as giving your objects a special phone number ๐Ÿ“ž that anyone can dial.

In Python terms, any object that implements the __call__ method becomes callable. This means you can use parentheses () after the object name, just like you would with a function:

  • โœจ Objects can maintain state between calls
  • ๐Ÿš€ More flexible than regular functions
  • ๐Ÿ›ก๏ธ Perfect for creating decorators and closures
  • ๐Ÿ“Š Great for implementing mathematical functions

๐Ÿ’ก Why Use Callable Objects?

Hereโ€™s why developers love callable objects:

  1. Stateful Functions ๐Ÿ—„๏ธ: Unlike regular functions, callable objects can remember things between calls
  2. Clean API Design ๐ŸŽจ: Make complex behaviors feel simple and intuitive
  3. Decorator Pattern ๐Ÿ—๏ธ: Perfect for creating advanced decorators
  4. Factory Pattern ๐Ÿญ: Build function generators with configuration
  5. Mathematical Operations ๐Ÿ“: Implement complex mathematical concepts elegantly

Real-world example: Imagine a counter that tracks how many times itโ€™s been called ๐Ÿ“Š. With __call__, this becomes elegant and intuitive!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Simple Example

Letโ€™s start with a friendly example:

# ๐Ÿ‘‹ Hello, Callable Objects!
class Greeter:
    def __init__(self, greeting="Hello"):
        self.greeting = greeting  # ๐Ÿ’ฌ Store our greeting
        self.count = 0           # ๐Ÿ“Š Track how many greetings
    
    def __call__(self, name):
        self.count += 1
        return f"{self.greeting}, {name}! ๐ŸŽ‰ (Greeting #{self.count})"

# ๐ŸŽจ Create our greeter
greeter = Greeter("Welcome")

# โœจ Now we can call it like a function!
print(greeter("Python"))     # Welcome, Python! ๐ŸŽ‰ (Greeting #1)
print(greeter("World"))      # Welcome, World! ๐ŸŽ‰ (Greeting #2)
print(greeter("Friend"))     # Welcome, Friend! ๐ŸŽ‰ (Greeting #3)

# ๐Ÿ“Š Check how many greetings
print(f"Total greetings: {greeter.count}")  # Total greetings: 3

๐Ÿ’ก Explanation: Notice how we use greeter() with parentheses, just like a function! But unlike a function, it remembers how many times itโ€™s been called.

๐ŸŽฏ Common Patterns

Here are patterns youโ€™ll use daily:

# ๐Ÿ—๏ธ Pattern 1: Function with Configuration
class Multiplier:
    def __init__(self, factor):
        self.factor = factor  # ๐Ÿ”ข Store multiplication factor
    
    def __call__(self, value):
        return value * self.factor

# ๐ŸŽจ Create specialized multipliers
double = Multiplier(2)
triple = Multiplier(3)
half = Multiplier(0.5)

print(double(10))   # 20
print(triple(10))   # 30
print(half(10))     # 5.0

# ๐ŸŽฎ Pattern 2: Stateful Counter
class Counter:
    def __init__(self, start=0):
        self.value = start
    
    def __call__(self, increment=1):
        self.value += increment
        return self.value

# ๐Ÿ“Š Create and use counter
counter = Counter(100)
print(counter())      # 101
print(counter(5))     # 106
print(counter(-10))   # 96

# ๐Ÿ”„ Pattern 3: Cached Function
class Memoize:
    def __init__(self):
        self.cache = {}  # ๐Ÿ’พ Store results
    
    def __call__(self, n):
        if n in self.cache:
            print(f"๐ŸŽฏ Cache hit for {n}!")
            return self.cache[n]
        
        # ๐ŸŒ Simulate expensive calculation
        result = sum(range(n + 1))
        self.cache[n] = result
        return result

# ๐Ÿš€ Use memoized calculator
calculator = Memoize()
print(calculator(100))    # Calculates: 5050
print(calculator(100))    # ๐ŸŽฏ Cache hit for 100! Returns: 5050

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: Smart Shopping Cart Validator

Letโ€™s build something real:

# ๐Ÿ›๏ธ Shopping cart with validation rules
class CartValidator:
    def __init__(self, max_items=10, max_total=1000):
        self.max_items = max_items      # ๐Ÿ“ฆ Maximum items allowed
        self.max_total = max_total      # ๐Ÿ’ฐ Maximum total price
        self.validations = []           # ๐Ÿ“‹ Track validation history
    
    def __call__(self, cart):
        # ๐ŸŽฏ Validate the cart
        errors = []
        
        # ๐Ÿ“ฆ Check item count
        if len(cart) > self.max_items:
            errors.append(f"โŒ Too many items! Max: {self.max_items}")
        
        # ๐Ÿ’ฐ Check total price
        total = sum(item['price'] * item['quantity'] for item in cart)
        if total > self.max_total:
            errors.append(f"โŒ Total too high! Max: ${self.max_total}")
        
        # ๐Ÿ“Š Record validation
        result = {
            'valid': len(errors) == 0,
            'errors': errors,
            'total': total,
            'item_count': len(cart)
        }
        self.validations.append(result)
        
        return result

# ๐ŸŽฎ Let's use it!
validator = CartValidator(max_items=5, max_total=500)

# ๐Ÿ›’ Test cart 1: Valid
cart1 = [
    {'name': 'Python Book', 'price': 29.99, 'quantity': 1, 'emoji': '๐Ÿ“˜'},
    {'name': 'Coffee', 'price': 4.99, 'quantity': 2, 'emoji': 'โ˜•'},
    {'name': 'Laptop Stand', 'price': 49.99, 'quantity': 1, 'emoji': '๐Ÿ’ป'}
]

result1 = validator(cart1)
print(f"Cart 1 valid: {result1['valid']} โœ…")
print(f"Total: ${result1['total']:.2f}")

# ๐Ÿ›’ Test cart 2: Too expensive
cart2 = [
    {'name': 'Gaming PC', 'price': 999.99, 'quantity': 1, 'emoji': '๐Ÿ–ฅ๏ธ'}
]

result2 = validator(cart2)
print(f"\nCart 2 valid: {result2['valid']} โŒ")
print(f"Errors: {result2['errors']}")

# ๐Ÿ“Š Check validation history
print(f"\nTotal validations performed: {len(validator.validations)}")

๐ŸŽฏ Try it yourself: Add a minimum total requirement and validate empty carts!

๐ŸŽฎ Example 2: Game Power-Up System

Letโ€™s make it fun with a game power-up system:

# ๐Ÿ† Power-up system for a game
class PowerUp:
    def __init__(self, name, emoji, effect_multiplier=1.5, duration=10):
        self.name = name
        self.emoji = emoji
        self.effect_multiplier = effect_multiplier
        self.duration = duration
        self.uses = 0  # ๐Ÿ“Š Track usage
        self.active = False
    
    def __call__(self, player_stats):
        # ๐ŸŽฏ Apply power-up to player
        self.uses += 1
        self.active = True
        
        # ๐Ÿš€ Boost player stats
        boosted_stats = {}
        for stat, value in player_stats.items():
            if isinstance(value, (int, float)):
                boosted_stats[stat] = value * self.effect_multiplier
            else:
                boosted_stats[stat] = value
        
        print(f"{self.emoji} {self.name} activated! (Use #{self.uses})")
        print(f"โฑ๏ธ Duration: {self.duration} seconds")
        
        return boosted_stats

# ๐ŸŽฎ Create different power-ups
speed_boost = PowerUp("Speed Boost", "โšก", 2.0, 15)
strength_potion = PowerUp("Strength Potion", "๐Ÿ’ช", 3.0, 10)
shield = PowerUp("Magic Shield", "๐Ÿ›ก๏ธ", 1.5, 20)

# ๐Ÿ‘ค Player stats
player = {
    'name': 'PyHero',
    'health': 100,
    'speed': 10,
    'strength': 15,
    'defense': 8
}

print("Original stats:", player)
print("\n" + "="*50 + "\n")

# ๐Ÿš€ Apply power-ups
fast_player = speed_boost(player)
print(f"Speed: {player['speed']} โ†’ {fast_player['speed']} ๐Ÿƒโ€โ™‚๏ธ")

strong_player = strength_potion(player)
print(f"Strength: {player['strength']} โ†’ {strong_player['strength']} ๐Ÿ’ช")

# ๐ŸŽฏ Chain power-ups!
print("\n๐ŸŽฎ Combining power-ups:")
shielded_stats = shield(fast_player)
print(f"Defense with speed boost: {shielded_stats['defense']} ๐Ÿ›ก๏ธโšก")

๐Ÿงฎ Example 3: Mathematical Function Builder

Create complex mathematical functions:

# ๐Ÿงฎ Mathematical function composer
class MathFunction:
    def __init__(self, expression, variables=['x']):
        self.expression = expression  # ๐Ÿ“ Mathematical expression
        self.variables = variables    # ๐Ÿ”ค Variable names
        self.call_count = 0          # ๐Ÿ“Š Track evaluations
        self.history = []            # ๐Ÿ“ˆ Store results
    
    def __call__(self, **kwargs):
        self.call_count += 1
        
        # ๐ŸŽฏ Evaluate the expression
        namespace = kwargs.copy()
        
        # ๐Ÿงฎ Add math functions to namespace
        import math
        namespace.update(vars(math))
        
        try:
            result = eval(self.expression, {"__builtins__": {}}, namespace)
            self.history.append((kwargs, result))
            return result
        except Exception as e:
            return f"โŒ Error: {e}"
    
    def plot_history(self):
        # ๐Ÿ“Š Simple ASCII plot
        if not self.history:
            return "No data to plot! ๐Ÿ“‰"
        
        print(f"\n๐Ÿ“ˆ Function: {self.expression}")
        print("โ”€" * 40)
        
        for inputs, result in self.history[-10:]:  # Last 10 results
            bar_length = int(abs(result) / 2) if result < 40 else 20
            bar = "โ–ˆ" * bar_length
            print(f"x={inputs.get('x', 0):>3}: {bar} {result:.2f}")

# ๐ŸŽจ Create mathematical functions
quadratic = MathFunction("a*x**2 + b*x + c", ['x', 'a', 'b', 'c'])
sine_wave = MathFunction("amplitude * sin(x * frequency)", ['x', 'amplitude', 'frequency'])
exponential = MathFunction("base ** x", ['x', 'base'])

# ๐Ÿงฎ Use quadratic function
print("๐Ÿ”ข Quadratic function: axยฒ + bx + c")
for x in range(-3, 4):
    y = quadratic(x=x, a=1, b=-2, c=1)
    print(f"f({x}) = {y}")

# ๐ŸŒŠ Generate sine wave
print("\n๐ŸŒŠ Sine wave:")
import math
for i in range(10):
    x = i * math.pi / 4
    y = sine_wave(x=x, amplitude=10, frequency=1)
    print(f"sin({i}ฯ€/4) * 10 = {y:.2f}")

# ๐Ÿ“ˆ Plot the history
quadratic.plot_history()

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Topic 1: Decorator Classes

When youโ€™re ready to level up, try callable decorators:

# ๐ŸŽฏ Advanced decorator with state
class TimedCache:
    def __init__(self, expiry_seconds=60):
        self.expiry_seconds = expiry_seconds
        self.cache = {}  # ๐Ÿ’พ Store results with timestamps
        self.func = None
    
    def __call__(self, *args, **kwargs):
        # ๐ŸŽจ If used as decorator
        if self.func is None and len(args) == 1 and callable(args[0]):
            self.func = args[0]
            return self
        
        # ๐Ÿš€ If used as function call
        import time
        key = str(args) + str(kwargs)
        
        # ๐Ÿ” Check cache
        if key in self.cache:
            result, timestamp = self.cache[key]
            if time.time() - timestamp < self.expiry_seconds:
                print(f"โšก Cache hit! (Age: {time.time() - timestamp:.1f}s)")
                return result
            else:
                print(f"โŒ Cache expired! (Age: {time.time() - timestamp:.1f}s)")
        
        # ๐Ÿงฎ Calculate new result
        print("๐Ÿ”„ Computing result...")
        result = self.func(*args, **kwargs)
        self.cache[key] = (result, time.time())
        return result

# ๐Ÿช„ Use as decorator
@TimedCache(expiry_seconds=5)
def expensive_calculation(n):
    # ๐ŸŒ Simulate slow operation
    import time
    time.sleep(1)
    return sum(i**2 for i in range(n))

# ๐ŸŽฎ Test caching behavior
print(expensive_calculation(100))  # ๐Ÿ”„ Computing result...
print(expensive_calculation(100))  # โšก Cache hit!
import time
time.sleep(6)
print(expensive_calculation(100))  # โŒ Cache expired! ๐Ÿ”„ Computing result...

๐Ÿ—๏ธ Advanced Topic 2: Callable Chain Builder

For the brave developers, create chainable callables:

# ๐Ÿš€ Chainable data transformer
class DataPipeline:
    def __init__(self, name="Pipeline"):
        self.name = name
        self.transforms = []  # ๐Ÿ”„ List of transformations
        self.debug = False    # ๐Ÿ› Debug mode
    
    def __call__(self, data):
        # ๐ŸŽฏ Apply all transforms
        result = data
        
        for i, (name, func) in enumerate(self.transforms):
            if self.debug:
                print(f"Step {i+1}: {name} ๐Ÿ”„")
            result = func(result)
        
        return result
    
    def add(self, name, func):
        # โž• Add transformation
        self.transforms.append((name, func))
        return self  # ๐Ÿ”— Enable chaining
    
    def __repr__(self):
        steps = " โ†’ ".join(name for name, _ in self.transforms)
        return f"Pipeline: {steps} ๐Ÿš€"

# ๐ŸŽจ Create data processing pipeline
pipeline = DataPipeline("Text Processor")
pipeline.debug = True

# ๐Ÿ”— Chain transformations
(pipeline
    .add("Lowercase", lambda x: x.lower())
    .add("Remove Spaces", lambda x: x.replace(" ", "_"))
    .add("Add Emoji", lambda x: f"โœจ_{x}_โœจ")
    .add("Uppercase", lambda x: x.upper())
)

# ๐ŸŽฎ Process some data
text = "Hello Python World"
result = pipeline(text)
print(f"\nOriginal: '{text}'")
print(f"Result: '{result}'")
print(f"\n{pipeline}")

# ๐Ÿงฎ Mathematical pipeline
math_pipeline = DataPipeline("Math Operations")
(math_pipeline
    .add("Double", lambda x: x * 2)
    .add("Add 10", lambda x: x + 10)
    .add("Square", lambda x: x ** 2)
    .add("Half", lambda x: x / 2)
)

print(f"\n๐Ÿ”ข Math pipeline on 5: {math_pipeline(5)}")

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Forgetting to Return Values

# โŒ Wrong way - No return value!
class BadCalculator:
    def __call__(self, x, y):
        result = x + y  # ๐Ÿ˜ฐ Calculated but not returned!
        # Oops, forgot to return!

calc = BadCalculator()
print(calc(5, 3))  # None ๐Ÿ’ฅ

# โœ… Correct way - Always return something!
class GoodCalculator:
    def __call__(self, x, y):
        result = x + y
        return result  # ๐ŸŽฏ Don't forget this!

calc = GoodCalculator()
print(calc(5, 3))  # 8 โœจ

๐Ÿคฏ Pitfall 2: Mixing Class and Instance Calls

# โŒ Dangerous - Confusing class vs instance
class Processor:
    counter = 0  # โš ๏ธ Class variable
    
    def __call__(self, data):
        Processor.counter += 1  # ๐Ÿ˜ฐ Modifies class state
        return data.upper()

# This creates problems!
p1 = Processor()
p2 = Processor()
p1("hello")
p2("world")
print(Processor.counter)  # 2 - Shared state! ๐Ÿ’ฅ

# โœ… Safe - Use instance variables
class BetterProcessor:
    def __init__(self):
        self.counter = 0  # ๐Ÿ›ก๏ธ Instance variable
    
    def __call__(self, data):
        self.counter += 1  # โœ… Modifies instance state
        return data.upper()

p1 = BetterProcessor()
p2 = BetterProcessor()
p1("hello")
p2("world")
print(p1.counter, p2.counter)  # 1 1 - Separate state! โœจ

๐Ÿ› Pitfall 3: Infinite Recursion

# โŒ Infinite loop danger!
class RecursiveCaller:
    def __call__(self, n):
        if n <= 0:
            return 0
        # ๐Ÿ’ฅ This calls __call__ again!
        return n + self(n - 1)  # Recursive call

# This works but be careful!
counter = RecursiveCaller()
# print(counter(1000))  # ๐Ÿ’ฅ RecursionError!

# โœ… Better - Add recursion limit
class SafeRecursiveCaller:
    def __init__(self, max_depth=100):
        self.max_depth = max_depth
        self.current_depth = 0
    
    def __call__(self, n):
        if self.current_depth >= self.max_depth:
            raise ValueError(f"โŒ Max recursion depth ({self.max_depth}) exceeded!")
        
        self.current_depth += 1
        try:
            if n <= 0:
                return 0
            return n + self(n - 1)
        finally:
            self.current_depth -= 1

safe_counter = SafeRecursiveCaller(max_depth=10)
print(safe_counter(5))   # 15 โœ…
# print(safe_counter(20))  # โŒ ValueError!

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Clear Purpose: Make sure callable objects have a clear, single purpose
  2. ๐Ÿ“ Document Behavior: Always document what happens when the object is called
  3. ๐Ÿ›ก๏ธ Handle Errors: Include proper error handling in __call__
  4. ๐ŸŽจ Intuitive API: Make the calling convention feel natural
  5. โœจ State Management: Be careful with mutable state between calls
  6. ๐Ÿš€ Performance: Donโ€™t make __call__ too heavy - users expect function-like speed
  7. ๐Ÿงช Test Thoroughly: Test both the object methods and the callable behavior
# ๐ŸŽฏ Example of well-designed callable class
class RateLimiter:
    """
    Limits function calls to n times per minute.
    Usage: limiter = RateLimiter(10)  # 10 calls per minute
           if limiter(): do_something()
    """
    def __init__(self, calls_per_minute):
        self.calls_per_minute = calls_per_minute
        self.calls = []
        import time
        self.time = time
    
    def __call__(self):
        """Returns True if call is allowed, False otherwise."""
        now = self.time.time()
        # ๐Ÿงน Clean old calls
        self.calls = [t for t in self.calls if now - t < 60]
        
        if len(self.calls) < self.calls_per_minute:
            self.calls.append(now)
            return True
        return False
    
    def reset(self):
        """Reset the rate limiter."""
        self.calls = []
    
    def __repr__(self):
        return f"RateLimiter({self.calls_per_minute}/min, current: {len(self.calls)})"

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Smart Task Scheduler

Create a callable task scheduler that manages and executes tasks:

๐Ÿ“‹ Requirements:

  • โœ… Schedule tasks with priorities (high, medium, low)
  • ๐Ÿ• Execute tasks based on scheduled time
  • ๐Ÿ“Š Track execution history
  • ๐Ÿ”„ Support recurring tasks
  • ๐ŸŽจ Each task should have a fun emoji!

๐Ÿš€ Bonus Points:

  • Add task dependencies
  • Implement task cancellation
  • Create execution statistics
  • Add error handling with retry logic

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
# ๐ŸŽฏ Smart Task Scheduler with __call__!
import time
from datetime import datetime, timedelta
from collections import defaultdict

class TaskScheduler:
    def __init__(self, name="Scheduler"):
        self.name = name
        self.tasks = []  # ๐Ÿ“‹ List of scheduled tasks
        self.history = []  # ๐Ÿ“Š Execution history
        self.stats = defaultdict(int)  # ๐Ÿ“ˆ Statistics
    
    def add_task(self, name, func, priority="medium", 
                 scheduled_time=None, recurring=None, emoji="๐Ÿ“Œ"):
        """Add a task to the scheduler."""
        task = {
            'id': len(self.tasks) + 1,
            'name': name,
            'func': func,
            'priority': priority,
            'scheduled_time': scheduled_time or datetime.now(),
            'recurring': recurring,  # 'daily', 'hourly', None
            'emoji': emoji,
            'active': True,
            'executions': 0
        }
        self.tasks.append(task)
        print(f"{emoji} Task '{name}' scheduled! (Priority: {priority})")
        return task['id']
    
    def __call__(self, current_time=None):
        """Execute all due tasks."""
        current_time = current_time or datetime.now()
        executed = []
        
        # ๐ŸŽฏ Sort tasks by priority
        priority_order = {'high': 0, 'medium': 1, 'low': 2}
        due_tasks = [
            t for t in self.tasks 
            if t['active'] and t['scheduled_time'] <= current_time
        ]
        due_tasks.sort(key=lambda x: priority_order.get(x['priority'], 3))
        
        # ๐Ÿš€ Execute tasks
        for task in due_tasks:
            try:
                print(f"\n{task['emoji']} Executing: {task['name']}")
                result = task['func']()
                
                # ๐Ÿ“Š Update statistics
                task['executions'] += 1
                self.stats['total_executions'] += 1
                self.stats[f"{task['priority']}_tasks"] += 1
                
                # ๐Ÿ“ Record in history
                self.history.append({
                    'task': task['name'],
                    'time': current_time,
                    'result': result,
                    'status': 'success'
                })
                
                executed.append(task['name'])
                
                # ๐Ÿ”„ Handle recurring tasks
                if task['recurring']:
                    if task['recurring'] == 'hourly':
                        task['scheduled_time'] += timedelta(hours=1)
                    elif task['recurring'] == 'daily':
                        task['scheduled_time'] += timedelta(days=1)
                else:
                    task['active'] = False
                    
            except Exception as e:
                print(f"โŒ Task '{task['name']}' failed: {e}")
                self.history.append({
                    'task': task['name'],
                    'time': current_time,
                    'error': str(e),
                    'status': 'failed'
                })
                self.stats['failed_tasks'] += 1
        
        return executed
    
    def cancel_task(self, task_id):
        """Cancel a scheduled task."""
        for task in self.tasks:
            if task['id'] == task_id:
                task['active'] = False
                print(f"๐Ÿšซ Task '{task['name']}' cancelled!")
                return True
        return False
    
    def get_stats(self):
        """Get execution statistics."""
        print(f"\n๐Ÿ“Š Scheduler Statistics:")
        print(f"Total executions: {self.stats['total_executions']}")
        print(f"High priority: {self.stats['high_tasks']}")
        print(f"Medium priority: {self.stats['medium_tasks']}")
        print(f"Low priority: {self.stats['low_tasks']}")
        print(f"Failed tasks: {self.stats['failed_tasks']}")
        
        # ๐Ÿ“ˆ Success rate
        total = self.stats['total_executions'] + self.stats['failed_tasks']
        if total > 0:
            success_rate = (self.stats['total_executions'] / total) * 100
            print(f"Success rate: {success_rate:.1f}%")

# ๐ŸŽฎ Test the scheduler!
scheduler = TaskScheduler("Daily Task Manager")

# ๐Ÿ“ Define some tasks
def backup_data():
    print("๐Ÿ’พ Backing up data...")
    return "Backup completed"

def send_report():
    print("๐Ÿ“ง Sending daily report...")
    return "Report sent"

def cleanup_temp():
    print("๐Ÿงน Cleaning temporary files...")
    return "Cleanup done"

def water_plants():
    print("๐ŸŒฑ Watering the plants...")
    return "Plants watered"

# ๐ŸŽฏ Schedule tasks
scheduler.add_task("Backup", backup_data, "high", 
                  datetime.now(), "daily", "๐Ÿ’พ")
scheduler.add_task("Report", send_report, "medium", 
                  datetime.now(), "daily", "๐Ÿ“ง")
scheduler.add_task("Cleanup", cleanup_temp, "low", 
                  datetime.now(), None, "๐Ÿงน")
scheduler.add_task("Plants", water_plants, "medium", 
                  datetime.now() + timedelta(hours=1), "daily", "๐ŸŒฑ")

# ๐Ÿš€ Execute current tasks
print("\n" + "="*50)
print("๐ŸŽฏ Running scheduler...")
executed = scheduler()
print(f"\nโœ… Executed tasks: {executed}")

# ๐Ÿ“Š Check statistics
scheduler.get_stats()

# ๐Ÿ”„ Simulate next day
print("\n" + "="*50)
print("๐ŸŒ… Next day...")
tomorrow = datetime.now() + timedelta(days=1)
executed = scheduler(tomorrow)
print(f"\nโœ… Executed tasks: {executed}")

# ๐Ÿ“Š Final stats
scheduler.get_stats()

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Create callable objects using the __call__ method ๐Ÿ’ช
  • โœ… Build stateful functions that remember between calls ๐Ÿง 
  • โœ… Implement advanced patterns like decorators and factories ๐Ÿ—๏ธ
  • โœ… Avoid common pitfalls with proper state management ๐Ÿ›ก๏ธ
  • โœ… Design intuitive APIs using callable objects ๐ŸŽฏ

Remember: Callable objects are like functions with superpowers - they can remember, adapt, and evolve! ๐Ÿฆธโ€โ™‚๏ธ

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered callable objects in Python!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the task scheduler exercise
  2. ๐Ÿ—๏ธ Create your own callable classes for real projects
  3. ๐Ÿ“š Explore how popular libraries use __call__ (like Djangoโ€™s forms)
  4. ๐ŸŒŸ Move on to our next tutorial: __new__ vs __init__

Remember: Every Python expert started where you are now. Keep experimenting with callable objects, and soon youโ€™ll be creating elegant, powerful APIs that feel magical to use! ๐Ÿš€


Happy coding! ๐ŸŽ‰๐Ÿš€โœจ