TechWayFit
● Complete Learning Series Python & AI Engineering for .NET Developers

Functional Python: Lambdas & Decorators

Tech Buddy June 24, 2026 3 min read
Functional Python: Lambdas & Decorators

Python is a multi-paradigm language, and functional programming is one of its strong suits. If you've used C# LINQ, delegates, or Func<T>, you'll find Python's functional features immediately familiar — just with different syntax and more expressive runtime behavior.

Decorators are particularly important for AI engineering. The retry logic, logging, caching, and timing wrappers you need for robust AI applications are all idiomatically implemented as decorators in Python.

Lambdas: One-Line Functions

Python's lambda creates an anonymous function — equivalent to C#'s lambda expressions or Func<T, TResult>. The restriction: a Python lambda can only contain a single expression, not a statement block.

C# Lambda / Func
Func<string, int> countWords =
                          s => s.Split(' ').Length;
                      
                      // LINQ with lambda
                      var sorted = responses
                          .OrderByDescending(r => r.Tokens)
                          .ToList();
Python Lambda
count_words = lambda s: len(s.split())
                      
                      # sorted() with key lambda
                      sorted_responses = sorted(
                          responses,
                          key=lambda r: r["tokens"],
                          reverse=True,
                      )

Where Lambdas Shine in AI Code

responses = [
                          {"id": "r1", "text": "Short answer.", "tokens": 12, "score": 0.91},
                          {"id": "r2", "text": "A longer, more detailed response.", "tokens": 38, "score": 0.85},
                          {"id": "r3", "text": "Medium length.", "tokens": 22, "score": 0.95},
                      ]
                      
                      # Sorting — lambda as sort key
                      by_score = sorted(responses, key=lambda r: r["score"], reverse=True)
                      by_tokens = sorted(responses, key=lambda r: r["tokens"])
                      
                      # Filtering — lambda in filter()
                      high_quality = list(filter(lambda r: r["score"] >= 0.9, responses))
                      
                      # Transformation — lambda in map()
                      texts_only = list(map(lambda r: r["text"].strip(), responses))
                      
                      # Min/max with key
                      best = max(responses, key=lambda r: r["score"])
                      shortest = min(responses, key=lambda r: r["tokens"])
                      
                      print(f"Best: {best['id']} (score={best['score']})")

Higher-Order Functions

Python functions are first-class objects. You can pass them as arguments, return them from functions, and store them in data structures — just like C# delegates or Func types.

from typing import Callable
                      
                      def apply_to_responses(
                          responses: list[dict],
                          transform: Callable[[dict], dict],
                      ) -> list[dict]:
                          """Apply any transformation function to each response."""
                          return [transform(r) for r in responses]
                      
                      
                      # Pass different functions to the same pipeline
                      def normalize_score(r: dict) -> dict:
                          return {**r, "score": round(r["score"], 2)}
                      
                      def add_word_count(r: dict) -> dict:
                          return {**r, "word_count": len(r["text"].split())}
                      
                      
                      normalized = apply_to_responses(responses, normalize_score)
                      with_counts = apply_to_responses(responses, add_word_count)
                      
                      # Compose transformations with a pipeline runner
                      def pipeline(responses: list[dict], *steps: Callable) -> list[dict]:
                          result = responses
                          for step in steps:
                              result = apply_to_responses(result, step)
                          return result
                      
                      final = pipeline(responses, normalize_score, add_word_count)
                      print(final[0])  # includes both score and word_count

Decorators: Python's Most Powerful Pattern

A decorator wraps a function to add behavior before, after, or around it — without modifying the original function. If you've used C# attributes like [Authorize], [Cache], or custom action filters in ASP.NET, you already understand the concept. Python decorators are more powerful because they're regular functions, applied at runtime.

How Decorators Work (Under the Hood)

import functools
                      
                      # A decorator is a function that takes a function and returns a function
                      def my_decorator(func):
                          @functools.wraps(func)  # preserves the original function's __name__ and __doc__
                          def wrapper(*args, **kwargs):
                              print(f"Before {func.__name__}")
                              result = func(*args, **kwargs)  # call the original
                              print(f"After {func.__name__}")
                              return result
                          return wrapper
                      
                      
                      @my_decorator
                      def call_llm(prompt: str) -> str:
                          return f"Response to: {prompt}"
                      
                      # @my_decorator is syntactic sugar for:
                      # call_llm = my_decorator(call_llm)
                      
                      result = call_llm("Hello")
                      # Output:
                      # Before call_llm
                      # After call_llm

Production Decorators for AI Workloads

1. Timing Decorator

import time
                      import functools
                      import logging
                      
                      logger = logging.getLogger(__name__)
                      
                      
                      def timed(func):
                          """Log execution time of any function."""
                          @functools.wraps(func)
                          def wrapper(*args, **kwargs):
                              start = time.monotonic()
                              try:
                                  result = func(*args, **kwargs)
                                  return result
                              finally:
                                  elapsed = (time.monotonic() - start) * 1000
                                  logger.info(f"{func.__name__} completed in {elapsed:.1f}ms")
                          return wrapper
                      
                      
                      @timed
                      def embed_documents(texts: list[str]) -> list[list[float]]:
                          """Embed multiple texts — timing tells us if the embedding model is slow."""
                          # ... implementation
                          return [[0.1, 0.2, 0.3]] * len(texts)
                      
                      
                      docs = embed_documents(["Hello", "World"])  # logs timing automatically

2. Retry Decorator (Parameterized)

import time
                      import functools
                      from typing import Type
                      
                      def retry(
                          exceptions: tuple[Type[Exception], ...] = (Exception,),
                          max_attempts: int = 3,
                          delay: float = 1.0,
                          backoff: float = 2.0,
                      ):
                          """
                          Parameterized retry decorator.
                          Usage: @retry(exceptions=(RateLimitError,), max_attempts=5)
                          """
                          def decorator(func):
                              @functools.wraps(func)
                              def wrapper(*args, **kwargs):
                                  current_delay = delay
                                  for attempt in range(max_attempts):
                                      try:
                                          return func(*args, **kwargs)
                                      except exceptions as e:
                                          if attempt == max_attempts - 1:
                                              raise
                                          logger.warning(
                                              f"{func.__name__} attempt {attempt+1} failed: {e}. "
                                              f"Retrying in {current_delay:.1f}s..."
                                          )
                                          time.sleep(current_delay)
                                          current_delay *= backoff
                              return wrapper
                          return decorator
                      
                      
                      from openai import OpenAI
                      
                      @retry(
                          exceptions=(openai.RateLimitError, openai.InternalServerError),
                          max_attempts=5,
                          delay=1.0,
                          backoff=2.0,
                      )
                      def call_claude(client: OpenAI, prompt: str) -> str:
                          response = client.chat.completions.create(
                              model="claude-3-5-sonnet-20241022",
                              max_tokens=512,
                              messages=[{"role": "user", "content": prompt}],
                          )
                          return response.choices[0].message.content

3. Caching Decorator

import functools
                      import hashlib
                      import json
                      
                      def cached_llm_call(func):
                          """Simple in-memory cache for LLM responses — avoid re-querying identical prompts."""
                          cache: dict[str, str] = {}
                      
                          @functools.wraps(func)
                          def wrapper(*args, **kwargs):
                              # Create a stable cache key from arguments
                              key_data = {"args": str(args), "kwargs": sorted(kwargs.items())}
                              cache_key = hashlib.md5(json.dumps(key_data).encode()).hexdigest()
                      
                              if cache_key in cache:
                                  logger.debug(f"Cache hit for {func.__name__}")
                                  return cache[cache_key]
                      
                              result = func(*args, **kwargs)
                              cache[cache_key] = result
                              return result
                      
                          wrapper.cache = cache  # expose cache for inspection/clearing
                          return wrapper
                      
                      
                      @cached_llm_call
                      def summarize(client, text: str, max_sentences: int = 3) -> str:
                          return client.complete(f"Summarize in {max_sentences} sentences:\n{text}")
                      
                      
                      # Or use Python's built-in lru_cache for pure functions:
                      from functools import lru_cache
                      
                      @lru_cache(maxsize=256)
                      def get_embedding(text: str) -> tuple[float, ...]:
                          """Cache embeddings — identical texts should return identical vectors."""
                          # embedding API call
                          return tuple([0.1, 0.2, 0.3])  # placeholder

4. Validator Decorator

def validate_prompt(max_length: int = 50_000):
                          """Validate prompt inputs before they hit the API."""
                          def decorator(func):
                              @functools.wraps(func)
                              def wrapper(*args, **kwargs):
                                  # Find the prompt argument
                                  prompt = kwargs.get("prompt") or (args[1] if len(args) > 1 else None)
                                  if prompt is None:
                                      raise ValueError("No prompt argument found")
                                  if not isinstance(prompt, str):
                                      raise TypeError(f"prompt must be str, got {type(prompt).__name__}")
                                  if not prompt.strip():
                                      raise ValueError("prompt cannot be empty or whitespace")
                                  if len(prompt) > max_length:
                                      raise ValueError(f"prompt too long: {len(prompt)} > {max_length} chars")
                                  return func(*args, **kwargs)
                              return wrapper
                          return decorator
                      
                      
                      @validate_prompt(max_length=10_000)
                      def analyze_code(client, prompt: str) -> str:
                          return client.complete(prompt)

Stacking Decorators

# Decorators are applied bottom-up (closest to the function first)
                      @timed
                      @retry(exceptions=(openai.RateLimitError,), max_attempts=3)
                      @validate_prompt(max_length=10_000)
                      def production_call(client: OpenAI, prompt: str) -> str:
                          """A fully hardened LLM call: validated, retried, and timed."""
                          response = client.chat.completions.create(
                              model="claude-3-5-sonnet-20241022",
                              max_tokens=512,
                              messages=[{"role": "user", "content": prompt}],
                          )
                          return response.choices[0].message.content
                      
                      # Execution order: validate → retry → time → actual call

Key Takeaways

  • Python lambda is equivalent to C# inline lambdas — use for simple sort keys, filter predicates, and map transformations
  • Functions are first-class objects: store them in lists, pass as arguments, return from functions — enables powerful pipeline composition
  • Decorators wrap functions to add cross-cutting behavior (timing, retry, caching, validation) without modifying the original code
  • Parameterized decorators use a three-level function nesting: decorator_factory() → decorator() → wrapper()
  • Always use @functools.wraps(func) in decorators to preserve the wrapped function's metadata
  • Stack decorators to compose behaviors — they apply bottom-up, closest to the function definition first

Comments

Loading comments…

Leave a comment