Introduction
A common, mundane need in software engineering, is to provide and share some utility code around your project.
Think of a toSnakeCase that turns a String from CamelCase into camel_case, or an isBusinessDay that tells you whether a Date falls on a working day. Both of those types come from your standard library, so you cannot open their sources and add these customised methods. And if your company keeps its shared business data in a common library, you do not want to fill that library with helpers that only one team's product will ever need.
Every language here has a way to accomplish this, but they are not equally safe.
Python: monkey-patching, global and silent
Python lets you attach a new attribute directly onto a class object at runtime, for any plain Python class (not the handful of C-implemented builtins like str or datetime.date, which rejects it). The mechanism has no scoping: the moment that code runs anywhere in the process, every import of that class, anywhere in the program, sees the change.
# billing_lib/models.py: a third-party library's pure-Python class
class OrderDate:
def __init__(self, value):
self.value = value
def weekday(self):
return self.value.weekday()
# Some other module, imported for an unrelated reason, does this at import time:
def is_business_day(self):
return self.weekday() < 5
OrderDate.is_business_day = is_business_day
# Now EVERY OrderDate anywhere in the process has this method,
# including in modules that never imported the one that added it,
# and including if two different libraries both patch the same name
# with different implementations. Whichever import runs last wins,
# silently, with no error.TypeScript: prototype pollution, and it reaches further
Patching Array.prototype or String.prototype has the same global, unscoped problem as Python monkey-patching, and it reaches further: browser extensions, bundled third-party scripts and your own code all share one mutable global prototype per built-in type. TypeScript does not stop this either. It just makes you tell the compiler to look away first, through a global declare block, before it lets the assignment through.
declare global {
interface Array<T> {
last(): T | undefined;
}
}
Array.prototype.last = function () {
return this[this.length - 1];
};
// Every array in the entire process, including ones created by
// libraries that have never heard of your code, now has .last().
// If two dependencies both add a method named .last() with different
// behavior, one silently overwrites the other at load order time.Java: safe, but backwards
Java has no extension mechanism at all. The pragmatic workaround is a static helper class, which is safe (no global mutation, nothing else is affected) but reads backwards from how you'd describe the operation: thing.doStuff() becomes Helper.doStuff(thing), and chaining several such calls nests instead of reading left-to-right.
class DateUtils {
static boolean isBusinessDay(LocalDate date) {
return date.getDayOfWeek().getValue() < 6;
}
static LocalDate nextBusinessDay(LocalDate date) {
LocalDate next = date.plusDays(1);
return isBusinessDay(next) ? next : nextBusinessDay(next);
}
static LocalDate addDays(LocalDate date, int days) {
return date.plusDays(days);
}
static String formatShort(LocalDate date) {
return date.toString();
}
}
// Reads backwards from the concept: "check if the date is a business
// day" becomes "call DateUtils with the date":
if (DateUtils.isBusinessDay(today)) {
scheduleDelivery();
}
// And chaining several of these nests instead of reading left to right:
DateUtils.formatShort(DateUtils.nextBusinessDay(DateUtils.addDays(today, 5)));Scala: safe, locally scoped, reads naturally
Scala 3's extension keyword lets you add a method to any type, including ones you don't own, that reads exactly like a real method call at the use site, but is lexically scoped: it only applies where you've imported it. No other code's view of LocalDate changes, ever. Here's the same four operations as Java's DateUtils:
// file DateUtils.scala
extension (date: LocalDate)
def isBusinessDay: Boolean = date.getDayOfWeek.getValue < 6
def nextBusinessDay: LocalDate =
val next = date.plusDays(1)
if next.isBusinessDay then next else next.nextBusinessDay
def addDays(n: Int): LocalDate = date.plusDays(n)
def formatShort: String = date.toString
// OtherClass.scala
import DateUtils.*
// Reads exactly like a real method, because from the call site's
// perspective, it might as well be one:
if today.isBusinessDay then scheduleDelivery(today.nextBusinessDay)Import that extension in one file, and only that file's view of LocalDate gains .isBusinessDay. A different file that hasn't imported it sees plain, unmodified LocalDate. There's no process-wide mutation, and no possibility of two unrelated extensions silently colliding the way two Python monkey-patches or two prototype patches can.
And because extension methods attach the same way real methods do, when the return type matches the type you are extending, chaining several of them reads in the order you would say it out loud: take today, add five days, roll forward to the next business day, format it. Compare the Java helper-class version with this:
today
.addDays(5)
.nextBusinessDay
.formatShortPredictable scope matters for agents working across files
Monkey-patching and prototype pollution are specifically dangerous in an AI-assisted workflow because their effects are non-local by design: an agent editing file A has no way to see that file B, imported transitively somewhere in the dependency graph, silently redefines a method on a type both files use. If the agent adds its own similarly-named patch, or relies on behavior that's actually coming from a patch it doesn't know exists, the bug that results is exactly the kind that's invisible from either file in isolation.
Scala's lexical scoping means an agent can work out an extension method's entire effect from the file it is editing and that file's imports. Nothing it writes can change how a type behaves in a file it never opened.
