Extending a function to accept null, under dynamic typing, can break a program which depends on the documented behavior of that function having thrown an exception for that case.
(defun strlen (s)
(assert (stringp s))
(len s))
(defun map-len-test ()
(mapcar (lambda (arg)
(ignerr (strlen arg)))
'("abc" nil "defg")))
(pprinl (map-len-test))
(defun strlen (s)
(if (null s)
0
(len s)))
(pprinl (map-len-test))
Output:
(3 nil 4)
(3 0 4)
The caller had one idea of substituting a value in the null argument case. The function changed behavior, implementing a different idea.
Only in a statically typed language in which the call with nil is impossible, and which provides no dynamic access to the compiler, can we be 100% confident in saying that extending the function to support nil is a non-breaking change.
If the is impossible, then the function has no behavior for that case; a program invoking that case doesn't exist in a translated, executable form and so there is nothing to break as far as the program is concerned.
It's possible for another program to break: a compilation test case which validates that code which calls that function with nil cannot be compiled. If the language supports dynamic access to the compiler, then an application can be written which can behave differently due to the change: an application which dynamically compiles a call to the function and now gets a valid result (compiled code) rather than an error.
In the dynamic world, it's a non-breaking change for applications that wisely don't rely on exceptions being thrown on bad inputs, and instead handle those themselves.
If you're changing the function, you cannot know that is not the case; at beast you can decide not to care.
Not caring is not the same as knowing.