Even without branching or loops, it's still a problem. Every line in a code block shares a namespace and state-space with each one another. If there's one very consistent lesson in the history of software engineering, it's that state is a necessary evil to be minimized. Stateful code almost always is less reliable and harder to reason about than pure code. Small functions keep the surface area of stateful interactions strictly delineated by the arguments and return parameters.
The more lines of code that exist in a block, even with linear control flow, the exponentially higher chance that one line inadvertently clobbers something that subsequent line depends on. Consider a function like
def foo (msg: str):
head_index = find_header(msg)
... # 50 lines of code
log(msg, head_index)
... # 50 lines of code
return msg[head_index]
Now say somebody adds some chunk of logic in the middle
def foo (msg: str):
head_index = find_header(msg)
...
head_index += offset_digest(...) # Needed for log formatting
log(msg, head_index)
...
return msg[head_index]
A seemingly innocuous change to the function, has inadvertently clobbered the value of a variable we're relying on in a slightly different context. With 50 lines of code separating each reference to the variable, it's very easy for this bug to fly under the radar unnoticed.
An engineering culture that emphasized small functions avoids this problem, because the change would instead move the logic to its own self-contained function. That avoids the problem of new code accidentally clobbering the old code.
def foo (msg: str):
head_index = find_header(msg)
...
log_offset(msg, head_index)
... # 50 lines of code
return msg[head_index]
def log_offset (msg, head_index):
head_index += offset_digest(...)
log(msg, head_index)