2025, Nov 01 05:00
How to Make SymPy Custom Functions Work with Modular Arithmetic: Use _eval_Mod for Mod
Learn how to integrate custom SymPy functions with modular arithmetic in composite expressions by implementing _eval_Mod, not __mod__. Includes examples.
Custom evaluation hooks in symbolic systems are the difference between expressions that merely print nicely and expressions that actually compute. If you are defining your own SymPy function and want it to participate in modular arithmetic across larger expressions, a naive operator overload won’t cut it. The right way is to plug into SymPy’s evaluation protocol for Mod.
Problem setup
The goal is to define a symbolic function that cannot be directly evaluated, but that still supports computing certain properties such as modular remainders. A direct operator overload seems to handle the simplest case, but breaks as soon as the function appears inside a larger expression.
from sympy import Function
class Buzz(Function):
@classmethod
def eval(cls, k):
pass
def __mod__(self, m: int) -> int:
val, = self.args
return (val + 1) % m
print(Buzz(7) % 3)
# 2
print((Buzz(7) + 1) % 3)
# Mod(Buzz(7) + 1, 3)
print((Buzz(7) + 1) % 3 == 0)
# False
Directly applying % to a single instance works, but wrapping that instance inside a sum obstructs the evaluation. The expectation was that the remainder of a sum could be computed from the remainders of its parts, but the expression remains symbolic.
Why this happens
In SymPy, the % operator on expressions does not call your object’s __mod__ in the way you might expect. Instead, Expr.__mod__ constructs a symbolic Mod(self, other). From there, SymPy asks the Mod object how to evaluate, and Mod in turn looks for a method named _eval_Mod on the pieces of the expression. If your custom function doesn’t implement _eval_Mod, the expression typically stays unevaluated in composite contexts.
The right way: implement _eval_Mod
To make a custom symbolic function work with modular arithmetic inside larger expressions, implement the _eval_Mod method. You do not need a __mod__ overload for this use case because the expression-level protocol already routes remainder computations through Mod, which queries _eval_Mod.
from sympy import Function, Expr, Integer
class Zed(Function):
@classmethod
def eval(cls, k):
pass
def _eval_Mod(self, modv: Expr) -> Integer:
if not isinstance(modv, Integer):
raise TypeError
arg0, = self.args
return (arg0 + 1) % modv
print(Zed(7) % 3)
# 2
print((Zed(7) + 1) % 3)
# 0
This makes the function participate in expression-level modular computations. The single-instance case still works, and, crucially, sums that contain the function now reduce under % as expected.
Scope and observed behavior
The improvement above applies to cases like (Zed(7) + 1) % 3. An observed case such as Zed(7) * 2 % 3 may remain as Mod(2*Zed(7), 3). The demonstrated integration shows additive contexts becoming computable, while other shapes can continue to stay symbolic.
Related tools inside SymPy
SymPy’s polynomial routines support modular arithmetic directly. Some functions accept a modulus parameter, which can be useful when factoring or simplifying polynomials over finite rings.
from sympy import symbols, factor
x = symbols('x')
print(factor(x**2 + 1))
# x**2 + 1
print(factor(x**2 + 1, modulus=2))
# (x + 1)**2
Why this matters
There are workflows where actual integers are too large to materialize, so numbers exist only as symbolic constructions. Even in that regime you might still need to compute certain properties such as modular remainders. Integrating with SymPy’s evaluation hooks lets your custom function operate in composite expressions without forcing eager evaluation of the entire object.
Takeaways
To make custom SymPy functions cooperate with modular arithmetic across expressions, rely on SymPy’s expression protocol rather than operator overloading. Implement _eval_Mod on your function so Mod can evaluate it when it appears inside larger formulas. For polynomial work, remember that many routines accept a modulus argument. This approach keeps your objects symbolic while still making targeted computations, which is exactly what you want when the underlying integers are too large to store outright.
The article is based on a question from StackOverflow by sligocki and an answer by Oscar Benjamin.