Python
function attributes
programming
code practices
Python tips

Python function attributes - uses and abuses

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Python functions are objects, and like any object they can have arbitrary attributes attached to them. Function attributes are set with func.attr = value and accessed with func.attr. Legitimate uses include memoization caches, call counters, metadata for decorators, and plugin registration systems. Abuses include using function attributes as a substitute for classes, storing unrelated global state, or hiding critical application logic in attributes that are invisible to code reviewers.

Setting and Reading Function Attributes

python
1def greet(name):
2    greet.call_count += 1
3    return f"Hello, {name}!"
4
5greet.call_count = 0
6
7greet("Alice")
8greet("Bob")
9print(greet.call_count)  # 2

Function attributes persist across calls because the function object itself is a long-lived object in the module's namespace. This makes them useful for simple counters and caches without external state.

Built-in Function Attributes

python
1def calculate_area(radius):
2    """Calculate the area of a circle."""
3    import math
4    return math.pi * radius ** 2
5
6# Built-in attributes
7print(calculate_area.__name__)       # "calculate_area"
8print(calculate_area.__doc__)        # "Calculate the area of a circle."
9print(calculate_area.__module__)     # "__main__"
10print(calculate_area.__defaults__)   # None (no default args)
11print(calculate_area.__code__.co_varnames)  # ('radius',)
12
13# __dict__ holds custom attributes
14calculate_area.version = "1.0"
15print(calculate_area.__dict__)  # {'version': '1.0'}

Every function has __name__, __doc__, __module__, __defaults__, __annotations__, and __dict__. Custom attributes go into __dict__.

Use Case: Memoization Cache

python
1def fibonacci(n):
2    if n in fibonacci.cache:
3        return fibonacci.cache[n]
4    if n < 2:
5        result = n
6    else:
7        result = fibonacci(n - 1) + fibonacci(n - 2)
8    fibonacci.cache[n] = result
9    return result
10
11fibonacci.cache = {}
12
13print(fibonacci(10))  # 55
14print(fibonacci.cache)  # {0: 0, 1: 1, 2: 1, 3: 2, ..., 10: 55}

This pattern predates functools.lru_cache and is still useful when you need direct access to the cache (for invalidation, inspection, or serialization).

Use Case: Decorator Metadata

python
1def route(path, methods=None):
2    """Register a function as a web route handler."""
3    def decorator(func):
4        func.route_path = path
5        func.route_methods = methods or ["GET"]
6        return func
7    return decorator
8
9@route("/users", methods=["GET", "POST"])
10def users_handler(request):
11    pass
12
13@route("/health")
14def health_check(request):
15    pass
16
17# Framework can discover routes by inspecting attributes
18handlers = [users_handler, health_check]
19for handler in handlers:
20    print(f"{handler.route_methods} {handler.route_path} -> {handler.__name__}")
21# ['GET', 'POST'] /users -> users_handler
22# ['GET'] /health -> health_check

Web frameworks like Flask use this pattern internally to associate URL routes with handler functions.

Use Case: Plugin Registration

python
1# Registry pattern using function attributes
2registry = []
3
4def plugin(name=None, version="1.0"):
5    def decorator(func):
6        func.plugin_name = name or func.__name__
7        func.plugin_version = version
8        registry.append(func)
9        return func
10    return decorator
11
12@plugin(name="CSV Exporter", version="2.1")
13def export_csv(data):
14    return ",".join(str(x) for x in data)
15
16@plugin(name="JSON Exporter")
17def export_json(data):
18    import json
19    return json.dumps(data)
20
21for p in registry:
22    print(f"{p.plugin_name} v{p.plugin_version}")
23# CSV Exporter v2.1
24# JSON Exporter v1.0

Abuse: Using Attributes as Global State

python
1# BAD: function attributes as a poor substitute for a class
2def process_order(item):
3    process_order.total += item["price"]
4    process_order.items.append(item["name"])
5    process_order.count += 1
6
7process_order.total = 0
8process_order.items = []
9process_order.count = 0
10
11# This should be a class
12class OrderProcessor:
13    def __init__(self):
14        self.total = 0
15        self.items = []
16        self.count = 0
17
18    def process(self, item):
19        self.total += item["price"]
20        self.items.append(item["name"])
21        self.count += 1

When a function accumulates multiple related attributes, it should be refactored into a class. Function attributes lack initialization, encapsulation, and instance separation.

Abuse: Hidden Dependencies

python
1# BAD: critical state hidden in function attributes
2def authenticate(username, password):
3    # Where is authenticate.secret_key set? Who initializes it?
4    token = sign(username, authenticate.secret_key)
5    return token
6
7# Somewhere else, far away in the codebase...
8authenticate.secret_key = os.environ["SECRET_KEY"]
9
10# BETTER: explicit parameter or configuration object
11def authenticate(username, password, secret_key):
12    token = sign(username, secret_key)
13    return token

Function attributes are invisible in the function signature. Readers of the function have no way to know that authenticate.secret_key must be set before calling the function.

Common Pitfalls

  • No initialization guarantee: Unlike class __init__, there's no mechanism to ensure function attributes are set before the function is called. Accessing an unset attribute raises AttributeError.
  • Not thread-safe: Function attributes are shared mutable state across all threads. Concurrent access to func.counter += 1 causes race conditions. Use threading.Lock or avoid mutable function attributes in multi-threaded code.
  • Decorators can lose custom attributes: Wrapping a function with @functools.wraps preserves __name__ and __doc__ but not custom attributes. Use functools.update_wrapper with the updated parameter to copy __dict__.
  • Testing is harder: Function attributes persist between test cases because the function object is module-level. You must manually reset attributes in test setup/teardown, or your tests will interfere with each other.
  • Readability suffers at scale: A function with many attributes is a class in disguise. If you find yourself adding more than one or two attributes, refactor to a class with proper encapsulation.

Summary

  • Functions are objects — custom attributes are stored in func.__dict__
  • Good uses: call counters, memoization caches, decorator metadata, plugin registration
  • Bad uses: substitute for classes, hidden global state, critical configuration
  • Built-in attributes (__name__, __doc__, __module__) are always available
  • Function attributes are not thread-safe — use locks for concurrent access
  • If a function has more than one or two custom attributes, refactor to a class

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.