The other day I found myself writing a really long Python script full of small groups of “helper” functions. Each group only “helped” a single caller. Something felt off. What a mess. Hidden under all the clutter, the script had a fairly simple structure. There’s only one path through the code. Breaking it into separate files would only obscure the logic. So how could I make that more clear?

Functions Within Functions

Then I remembered Python allows nested (inner) function definitions – they have access to their enclosing scope too. Back when I mostly used Turbo Pascal and then Delphi this was a standard technique for encapsulating “helper” logic to make a program more readable.

Years of developing in C, C++ and Ruby had erased my habit of nesting functions. (Ruby allows them but they are pure functions without access to outer scopes.) C doesn’t allow nested functions at all. (The GNU C extensions allow nesting C functions, but not in C++) C++ allows lambdas but they aren’t used extensively as a code organization tool. Instead you may use namespaces and static class functions to group code together.

How Inner Functions Improve Code

A simple Python script which may be lengthy, but not complex, or a complex Rust program that’s not written in an OO-style will suffer if all your small helper functions live at the top level. Your IDE will present a long list of functions with no context. Similarly when reading with a plain text editor you get no context aside from your code comments and code placement.

IN an OO language you can use private and public methods but those are only two categories for functions. You hide the helper functions to an extent but that’s about it. Extensive sub-classing can help “organize” behavior but often creates fragmentation and is a well-known bad practice when applied widely. You may create a few classes without state to group helper and main functions together, with all the helpers declared private. That’s reasonable but seems like more boiler-plate than necessary when you don’t need any instances of the class or struct.

A simpler and direct solution is to just nest functions. Moving functions used by only one caller inside the caller cleans up the top-level name space seen by your IDE. It also shows to a human reader very clearly how the remaining top level functions behave.

Putting the helper functions inside the caller function communicates to the reader that they belong to that task only. Looking at it from another direction, don’t feel you must extract chunks of a sprawling hard to read function to the top level of the module: Maybe it makes sense to start by naming chunks and converting them to inner functions. Then the actual core logic of the big function becomes much easier to read, while not adding clutter to the parent module.

Advantages of Inline Code Style / Linear Style

Inlining refers to placing small pieces of logic directly in a funtion sequentially rather than factoring into many small functions. In this exchange John Carmack recommends placing singly called logic together in one linear function.

Using large comment blocks inside the major function to delimit the minor functions is a good idea for quick scanning, and often enclosing it in a bare braced section to scope the local variables and allow editor collapsing of the section is useful. I know there are some rules of thumb about not making functions larger than a page or two, but I specifically disagree with that now – if a lot of operations are supposed to happen in a sequential fashion, their code should follow sequentially.

In C and C++ you don’t have the slightly better option of actual functions, just scoped blocks and comments. But if you did it would seem like a logical additional step for code clarity while keeping encapsulation. That’s important:

Besides awareness of the actual code being executed, inlining functions also has the benefit of not making it possible to call the function from other places. That sounds ridiculous, but there is a point to it. As a codebase grows over years of use, there will be lots of opportunities to take a shortcut and just call a function that does only the work you think needs to be done. There might be a FullUpdate() function that calls PartialUpdateA(), and PartialUpdateB(), but in some particular case you may realize (or think) that you only need to do PartialUpdateB(), and you are being efficient by avoiding the other work. Lots and lots of bugs stem from this. Most bugs are a result of the execution state not being exactly what you think it is.

With inner functions you arguably get the best of both approaches: Better factoring of behavior and scoping local variables while preventing use outside the correct context.

More thoughts on linear vs highly compartmentalized sstyle, asserting Linear Code is More Readable.

Examples in Python, Rust and Pascal

Here are some trivial examples. Imagine instead you are calculating a complicated multi-step formula.

Python

def outer(x):
    m = 2
    n = 1

    def inner(y):
        return y * 10 * m + n
        
    return inner(x)

def main():
    print(f"{outer(9)}")

main()

The inner function has read-only access to its enclosing scope, but isn’t automatically a closure. The inner function accesses the values of the parent scope variables at call time. There’s more you can do with Python inner functions: How Nested Functions are Use in Pytho including making closures.

Rust

fn outer(x: i32) -> i32 {
	let mut m: i32 = 2;
	let mut n: i32 = 99;
	
	// This would be allowed
	n = 1;
	
	let inner = |y: i32| -> i32 {
		y * 10 * m + n		
	};
	
	// This is not allowed
	// m = 1; n=0;
		
	inner(x)
}

fn main() {
    println!("{}\n", outer(9));	
}

This is an example of using a closure. If you use a standard fn declaration inside another function you can’t access the enclosing scope at all. In some cases that’s fine for pure organization improvements. That way there’s no confusion about when the state of local variables gets read or written.

When using closures the borrow checker makes sure any variables used inside the closure from the enclosing scope are owned by the closure making writing to them outside the closure illegal.

Pascal

program nested_demo;

function outer(x: integer): integer;
var
	n: integer = 99;
	m: integer = 99;

	function inner(y: integer): integer;
	begin	
		inner := y * 10 * m + n
	end;
	
begin
	m:= 2; n := 1; 
	outer := inner(x)
end;

begin
	writeln(outer(9))
end.

Pascal is where I first used nested functions. It’s straightforward. Partly that’s due to Pascal’s variable declaration block style: All declarations are at the function scope and have no order, unlike Rust or C or most other languages.

This extends to the inner functions: They are not placed in the code block but as part of the outer function’s definition. So, you don’t have to reason about order of function or variable declaration or assignment. You’re only concerned with assignment or re-assignment and function call order. In this way it behaves superficially like Python. However Pascal allows assignment to the outer scope variables, and Python does not.

Unlike Python, Pascal inner functions can’t be converted to closures, though you can have “typed constants” which preserve state across function calls as with “static” in other languages (assuming you have only one thread.)

Javascript

Here’s a nice demonstration of better structuring through function nesting Refactoring with Function Nesting.

When to Use Nesting

  • Organization: When you have “helper” functions only needed by one main function that has the core algorithm.
  • Encapsulation: When you have a group of small functions that only use each other and they shouldn’t be seen or called by any others.
  • Readibility: When you have one large function carrying out a series of steps on the same data. Sometimes it’s not obvious how to break it into pieces that are anything but a series of calls passing their arguments from one to another. Go ahead and move those steps into the caller.

When not to Use Nesting

  • If you get more than two levels deep.
  • If you’re repeating inner functions in several caller functions
  • If you need to preserve state in the inner functions in a multi-threaded environment (this calls for a re-org or closures or classes.)

Classes and Closures

My point here is to talk about nesting functions as a tool for good code organization, not to promote closures over classes. Functional languages use closures to preserve state while OO languages have classes of objects to preserve state. In either case function nesting can be a useful way to structure code and document behavior regardless of state if your language allows it. Also, whether or not a language supports first-class functions (passing functions as values) you can still use nesting as an organization tool. See this tutorial: Python Inner Functions: What are They Good For? for more advanced uses like “closure factories.”

In Ruby and Java, and object-oriented languages generally, you can organize related functions as methods on a class even when the class has no state – it’s a decent approach when everything’s a class in the language. This is one way to group your “helper” functions.

Nesting functions are an encapsulation technique and code organization tool; they may or may not, depending on the language, also serve as closures to preserve state between calls. Rust, for instance, has both locally declared closures and allows inner functions which do not behave as closures.

Conclusion

Sometimes instead of adopting a new library or programming paradigm, nice quality of life improvements are right there in your language.