Is that Not-Implemented?
Hello, everyone! Welcome back to Cameron's Corner! This week, I want to discuss some metaprogramming utilities that are being considered for use in Narwhals. In this project, we have an adapter pattern where we create multiple adaptations that help bridge the gap between popular DataFrame tools and the Narwhals interface (which follows the Polars API).
This means that, for any given DataFrame (like a pandas.DataFrame), there is another class created that follows the Polars API (e.g., DataFrames have a .with_columns method instead of .assign) and operates on a pandas.DataFrame under the hood. This pattern is perfect for the problem at hand, but it becomes tricky to track all of the adapter classes that have been created against all of the backends (pandas, modin, duckdb, sqlframe, dask.dataframe, ibis, and more). Today, I am going to discuss a narrow slice of problems that arise with a varied backend like this: figuring out what methods are actually implemented on what backends!
Somewhat Implemented
If you've spent enough time working on large codebases—especially in Python—you've probably encountered functions that aren't fully implemented yet. Sometimes, developers mark unfinished functionality by raising NotImplementedError, a clear signal that something is still a work in progress. Other times, they might leave placeholder functions or simply assume that the presence of a return statement is enough to determine whether a function is actually doing something.
But what if you need to programmatically check whether a function is fully implemented? Maybe you're working with plugin architectures, enforcing API completeness, or just trying to maintain better code hygiene. This isn't just an academic exercise—real-world projects often need mechanisms to track function completeness, whether for runtime validation, testing, or documentation purposes.
Today, we'll explore different ways to approach this problem. We'll start by using Python’s ast module to analyze function definitions, then compare various registration strategies. Along the way, we’ll highlight the potential pitfalls of each approach and show how decorators can help maintain consistency while avoiding update anomalies.
Take a look at the following functions:
def f():
return 42
def g():
raise NotImplementedError()
def h():
if True:
raise NotImpelmentedError()
return 42
f()
42
The function f returns a constant value, 42, while g immediately raises a NotImplementedError. Function h is meant to raise NotImplementedError conditionally—and as you can see it will always raise in the example above.
Attempting to execute g() confirms that it raises an exception:
g()
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
Cell In[2], line 1
----> 1 g()
Cell In[1], line 4, in g()
3 def g():
----> 4 raise NotImplementedError()
NotImplementedError:
This leads us to the next step: detecting whether a function is implemented or not using AST (Abstract Syntax Tree) parsing.
Checking for NotImplemented with AST
The following function analyzes a function's source code using ast to determine if it is implemented. We can use the ast module to statically analyze the source code of the target function. All we need to do is check if a function has a return and whether that function ever raises a NotImplementedError.
import ast
from inspect import getsource
from textwrap import dedent
from typing import Callable, Any
def is_implemented(func: Callable[..., Any]) -> bool:
class NotImplementedVisitor(ast.NodeVisitor):
def __init__(self) -> None:
self.has_notimplemented = False
self.has_return = False
super().__init__()
def visit_Return(self, node: ast.Return) -> None: # noqa: N802
self.has_return = True
def visit_Raise(self, node: ast.Raise) -> None: # noqa: N802
if (
# raise NotImpelmentedError()
(isinstance(node.exc, ast.Call) and node.exc.func.id == "NotImplementedError")
# raise NotImpelementedError
or (isinstance(node.exc, ast.Name) and node.exc.id == "NotImplementedError")
):
self.has_notimplemented = True
source = dedent(getsource(func))
tree = ast.parse(source)
v = NotImplementedVisitor()
v.visit(tree)
return v.has_return or not v.has_notimplemented
print(
f'{is_implemented(f) = }',
f'{is_implemented(g) = }',
f'{is_implemented(h) = }',
sep='\n',
)
is_implemented(f) = True
is_implemented(g) = False
is_implemented(h) = True
This function checks if a function contains a return statement or avoids raising NotImplementedError. The following truth table exemplifies the cases in which we would deem a function as implemented or not. You'll notice that we can only statically determine if a function is considered to be not implemented if it raises NotImplementedError and does not contain a return statement.
return |
NotImpelmentedError |
is_implemented |
---|---|---|
True |
False |
True |
True |
True |
True |
False |
False |
True |
False |
True |
False |
AST Parsing Leads to Ambiguity
As you can see, this approach can catch the simplest case of NotImplementedErrors, but if a function's implementation relies on a modality, then this approach quickly falls apart. Furthermore, if this error is raised by a downstream called a function, then we won't be able to detect it here either. Let's take a look at some other approaches to see if we can create a more robust solution.
Tagging Functions
Instead of burying a NotImplementedError inside of a function, what if we decided to use a tagging system instead? We can tag functions that are supposed to be not implemented, making it easy for us to determine whether or not something should not be called.
As the most naive approach, we can simply add an attribute to the defined functions to determine whether or not they are marked as NotImplemented. The existence of this attribute will be our key for future checks:
def f():
return 42
def g():
return 'hello world'
# local registration: the function has an indicative attribute
f.not_implemented = True
print(
f"{hasattr(f, 'not_implemented') = }",
f"{hasattr(g, 'not_implemented') = }",
sep='\n'
)
hasattr(f, 'not_implemented') = True
hasattr(g, 'not_implemented') = False
However, we have now decoupled our check from the actual call of the function—notice that we claim f is not implemented, but when we call it:
f()
42
The code still works because we have introduced an update anomaly- what we declare as NotImplemented can very obviously deviate from that ground truth. To fix this we would need to reassign the variable f to a new function that appropriately errors out. Considering our unit of work here is a function, a decorator provides the perfect pattern for us to implement this feature. Specifically...
When a function is defined, we want to mark it as not implemented.
When a function is called and it is marked as not implemented, we want it to raise NotImplementedError.
from functools import wraps
def not_implemented(msg):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
raise NotImplementedError(msg)
wrapper.not_implemented = True # store info on the method itself
return wrapper
return decorator
@not_implemented('I haven’t written this code yet')
def f(self):
pass
def g(self):
return 42
print(
f"{hasattr(f, 'not_implemented') = }",
f"{hasattr(g, 'not_implemented') = }",
sep='\n'
)
hasattr(f, 'not_implemented') = True
hasattr(g, 'not_implemented') = False
f()
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
Cell In[7], line 1
----> 1 f()
Cell In[6], line 7, in not_implemented.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
5 @wraps(func)
6 def wrapper(*args, **kwargs):
----> 7 raise NotImplementedError(msg)
NotImplementedError: I haven’t written this code yet
Perfect! We have removed this update anomaly, and while we can check each function to see if it is implemented, we have no real way of easily finding all of our not implemented functions in a particular namespace. inspect.getmembers would probably come to the rescue here, but what if we kept track of these tagged functions in some type of globally accessible registry?
Global Registration
In this case, we will use an external data structure (set()) to keep track of each of the functions. This approach does not involve us adding arbitrary attributes to a function and gives us a way to programmatically access any decorated function with ease.
from functools import wraps
registry = set()
def not_implemented(msg):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
raise NotImplementedError(msg)
registry.add(wrapper)
return wrapper
return decorator
@not_implemented('I haven’t written this code yet')
def f():
pass
def g():
pass
print(
f"{f in registry = }",
f"{g in registry = }",
sep='\n'
)
f in registry = True
g in registry = False
All of our registered functions exist here:
registry
{<function __main__.f()>}
Dealing with Modalities
Let's go back to our tricky problem where our function may or may not be implemented depending some combination of arguments passed to it. While I personally feel that these types of modalities should be factored out into two different functions, we often need to work with the code that we have at hand.
Take a look at the following code. The only way we can determine if it is NotImpelmented is to execute it with different combinations of arguments and catch exceptions.
def f(mode=False):
if mode:
raise NotImplementedError()
return 42
But what if we could parameterize our not_implemented decorator? What if we could declare specific combinations of arguments that will lead to this error, and any combination of unspecified arguments will work as expected?
With a bit of use from the inspect module, we can store function objects alongside their specific args/kwargs into a registry, and we can then check this registry for any specific combinations of functions and their arguments to see if something is expected to be NotImplemented.
from inspect import signature
from functools import wraps
registry = set()
def not_implemented(*args, **kwargs):
def decorator(func):
bound = signature(func).bind_partial(*args, **kwargs)
bound.apply_defaults()
registry.add((func, bound.args, frozenset(bound.kwargs)))
return func
return decorator
def check_not_implemented(func, *args, **kwargs):
bound = signature(func).bind_partial(*args, **kwargs)
bound.apply_defaults()
return (func, bound.args, frozenset(bound.kwargs)) in registry
@not_implemented(mode=True)
def f(mode=False):
if mode:
raise NotImplementedError()
return 42
print(
f"{check_not_implemented(f, False) = }",
f"{check_not_implemented(f, mode=False) = }",
f"{check_not_implemented(f, True) = }",
f"{check_not_implemented(f, mode=True ) = }",
sep='\n'
)
check_not_implemented(f, False) = False
check_not_implemented(f, mode=False) = False
check_not_implemented(f, True) = True
check_not_implemented(f, mode=True ) = True
Of course, this works provided the arguments we're passing are all hashable—if we wanted to also track unashable arguments, then we may need to resort to using a list for the registry instead of a set.
from inspect import signature
from functools import wraps
registry = set()
def not_implemented(*args, **kwargs):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if check_not_implemented(wrapper, *args, **kwargs):
raise NotImplementedError('You said this was not implemented!')
return func(*args, **kwargs)
bound = signature(func).bind_partial(*args, **kwargs)
bound.apply_defaults()
registry.add((wrapper, bound.args, frozenset(bound.kwargs)))
return wrapper
return decorator
def check_not_implemented(func, *args, **kwargs):
bound = signature(func).bind_partial(*args, **kwargs)
bound.apply_defaults()
return (func, bound.args, frozenset(bound.kwargs)) in registry
@not_implemented(mode=True)
def f(mode=False):
return 42
print(
f"{check_not_implemented(f, False) = }",
f"{check_not_implemented(f, mode=False) = }",
f"{check_not_implemented(f, True) = }",
f"{check_not_implemented(f, mode=True ) = }",
sep='\n'
)
check_not_implemented(f, False) = False
check_not_implemented(f, mode=False) = False
check_not_implemented(f, True) = True
check_not_implemented(f, mode=True ) = True
f()
42
f(mode=True)
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
Cell In[14], line 1
----> 1 f(mode=True)
Cell In[12], line 11, in not_implemented.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
8 @wraps(func)
9 def wrapper(*args, **kwargs):
10 if check_not_implemented(wrapper, *args, **kwargs):
---> 11 raise NotImplementedError('You said this was not implemented!')
12 return func(*args, **kwargs)
NotImplementedError: You said this was not implemented!
And there you have it! An opt-in modality-dependent manner to verify and force a given function to raise a NotImplementedErrors.
Wrap-Up
Detecting whether a function is implemented isn't always as straightforward as it seems. While AST parsing can catch simple cases where NotImplementedError is explicitly raised, it quickly falls apart when dealing with more complex execution paths, such as conditional implementations or deferred errors in downstream calls.
Tagging functions—whether through local attributes or global registries—provides a more flexible way to track incomplete implementations. However, these approaches introduce potential update anomalies unless they are tightly coupled to function execution. Using decorators to enforce function behavior at runtime can mitigate these issues while keeping the registration process explicit and organized.
Ultimately, the right approach depends on the context. If you're working on a large codebase with a well-defined API, tagging functions explicitly with decorators or registries ensures better tracking and maintenance. For dynamic or plugin-based architectures, runtime checks might be necessary to handle varying levels of completeness. No matter the strategy, being intentional about marking and managing incomplete functionality will make your codebase more maintainable, predictable, and easier to debug.
What do you think about metaprogramming utilities that are being considered for use in Narwhals? Let me know on the DUTC Discord server.
Talk to you all then!