Friday, 25 September 2026

Counting Lines in Python

We have some Python scripts that at some point count the number of lines in a file. We do so by invoking wc -l, like this:


def count_lines_wc(file_path: str) -> int:
    result = subprocess.run(
        ["wc", "-l", file_path],
        check=True,
        capture_output=True,
        text=True,
    )
    return int(result.stdout.split()[0])

Well, counting lines does not seem like a big deal, so why not just do it in pure Python? The immediate approach would be something like:


    def count_lines_1(file_path: str, encoding: str | None = None) -> int:
        """ counts the number of lines in a file, without loading it all in memory (so it can be used for big files)"""
        with open(file_path, "r", encoding=encoding) as fr:
            return sum(1 for _ in fr)   

The problem with that function when compared to wc -l is that we have to provide the encoding or use the default one (that depends on your system, it's what locale.getpreferredencoding() returns, that on Linux systems is utf-8, but on windows it's cp1252).

When we call "wc -l" we are not passing any encoding, so how does it do it? It just reads the file in binary mode and counts the bytes corresponding to a newline character, that is the 0X0A byte (or b"\n"). So we can do that in Python like this:


def count_lines_2(path: str) -> int:
    """ 
    behaves like 'wc -l' command, as it's byte based it does not have to take into account encoding, 
    but it can be used only for files that use \n as line separator (so Linux, Windows, 'modern' MacOS)
    """
    with open(path, "rb") as f:
        return sum(chunk.count(b"\n") # or b"0x0A"
            for chunk in iter(lambda: f.read(8192), b""))


So I'm reading chunks of n bytes (8192 bytes in this case) and counting the occurrences of the "\n" byte in it. Checking this with a GPT it gave me a more pythonic version:


def count_lines_3(file_path: str) -> int:
    """Counts newline bytes, like wc -l"""
    with open(file_path, "rb") as f:
        return sum(1 for _ in f)  # reads binary lines split by b'\n'

This version is using something I was not aware of. We know that the file object that we obtain after opening a file in text mode is an iterator that yields lines. But the file object that we obtain when opening a file in binary mode (in this cases it's a binary file object) is also an iterable/iterator that iterates bytes splitting by the b"\n" byte (and that b"\n" byte is also included in the bytes yielded in each iteration, save for the last 'line' if it lacks it).

We have an interesting difference depending on whether the last line of a file contains a newline character or not. The standard defines that text files should end with a newline character, but not everybody follows the standard. If we have a text file which last line does not end with a newline character, using wc -l (or my count_lines_2), that work by counting "\n"'s will not count the last line, while using my count_lines_3 will count it.

We can see that difference by executing the above functions with 2 versions of a same text file, one version with a newline character in its last line and another version without it, we get:


for file_name in files:
    print(f"-- Counting lines in {file_name}...")
    for fn in [count_lines_wc, count_lines_1, count_lines_2, count_lines_3]:
        print(f"{fn(file_name)} lines counted by {fn.__name__}")

# -- Counting lines in withEOL.txt...
# 4 lines counted by count_lines_wc
# 4 lines counted by count_lines_1
# 4 lines counted by count_lines_2
# 4 lines counted by count_lines_3

# -- Counting lines in noEOL.txt...
# 3 lines counted by count_lines_wc
# 4 lines counted by count_lines_1
# 3 lines counted by count_lines_2
# 4 lines counted by count_lines_3

Related to this, what if we want to count the lines of one file inside a zip? Obviously we can extract the file and use one of the above functions, but Python zipref.ZipFile allows us reading the file without extracting it. ZipFile.open opens a file in binary mode, and then we can either read it by chunks or iterate it, just as we did in the previous examples. So if we put the 2 previous files into a zip we have:



def count_lines_in_zip_file_1(zip_ref: zipfile.ZipFile, file_name: str) -> int:
    # works like wc -l: count newline bytes without extracting to disk.
    # "r" mode for zip_ref.open is equivalent to "rb" in the standard open, so we get bytes and can count b"\n".
    num_lines = 0
    with zip_ref.open(file_name, "r") as zipped_file:
        for chunk in iter(lambda: zipped_file.read(1024 * 1024), b""):
            num_lines += chunk.count(b"\n")
    return num_lines

def count_lines_in_zip_file_2(zip_ref: zipfile.ZipFile, file_name: str) -> int:
    # Count lines without extracting to disk. If the last line lacks a newline character, it won't be counted
    # "r" mode for zip_ref.open is equivalent to "rb" in the standard open, so we get bytes and can count b"\n".
    num_lines = 0
    with zip_ref.open(file_name, "r") as zipped_file:
        return sum(1 for _ in zipped_file)  # reads binary lines split by b'\n' (b'0x0A') and counts them, so it behaves like wc -l



zip_ref = zipfile.ZipFile("files.zip", "r")

files = ["withEOL.txt", "noEOL.txt"]

for file_name in files:
    print(f"-- Counting lines in {file_name} inside zip file...")
    for fn in [count_lines_in_zip_file_1, count_lines_in_zip_file_2]:
        print(f"{fn(zip_ref, file_name)} lines counted by {fn.__name__}")
        

# -- Counting lines in withEOL.txt inside zip file...
# 4 lines counted by count_lines_in_zip_file_1
# 4 lines counted by count_lines_in_zip_file_2

# -- Counting lines in noEOL.txt inside zip file...
# 3 lines counted by count_lines_in_zip_file_1
# 4 lines counted by count_lines_in_zip_file_2


Saturday, 12 September 2026

Python Enums missing_key

In a recent post about Python enums I mentioned the _missing_ method, that gets executed when accessing an enum member by value if that value is missing.


from enum import Enum
class Color(Enum):
    RED = "red"
    BLUE = "blue"
    @classmethod
    def _missing_(cls, value):
        value = value.lower()
        print(f"missing value: {value}")
        for member in cls:
            if member.value == value:
                return member
        return None

print(Color.RED is Color("red"))
# True

print(Color.RED is Color("rEd"))
# missing value: rEd
# True

We know that we can also get an enum member by name, using the [] syntax (subscripted access or key/name access), like color = Color["RED"], and well, it feels to me that it would be nice to have this kind missing functionality for that kind of access (let's call it 'missing_key'). I mean, defining a sort of missing_key method in our enum classes that would get invoked if the key/name used does not exist. And how can we hook this behaviour into the subscripted access to the enum? At the end of this post I talked about those __dunder_methods__ that work as behavioral hooks. We have hooks for invokation, attribute getting, attibute setting... and also one, __getitem__ for subscripted access. For this case, we'll be using the subscripted access in the class, so we have to implement it in a metaclass. The Enum class has EnumMeta as its metaclass, so we'll need our new metaclass to inherit from EnumMeta. When creating an Enum empowered with this "missing_key" functionality, it'll have to use our new metaclass, and inherit from Enum (or related child classes like IntEnum, StrEnum...).

`EnumMeta` implements the class-creation machinery (turning attributes into members, building `__members__` and `_value2member_map_`), but it relies on the base class (`Enum`) to provide the instance API and the initialization/constructor expectations used when creating each member. Without inheriting from `Enum`, those assumptions break and class creation fails or yields an object that isn't a proper enum.

So we define this metaclass, that when accessing a missing key will invoke a _missing_key method (if it exists) in the enum class.


from enum import EnumMeta, Enum

class MissingKeyEnum(EnumMeta):
    def __getitem__(cls, name):
        try:
            return super().__getitem__(name)
        except KeyError:
            print(f"Name '{name}' is not a valid member of {cls.__name__}")
            # Call a dedicated missing-name handler if present
            if hasattr(cls, "_missing_key"):
                return cls._missing_key(name)
            return None


An now we define a class that leverages the metaclass to allow case insensitive subscripted access:


class Query(Enum, metaclass=MissingKeyEnum):
    SELECT = "select"
    INSERT = "insert"
    UPDATE = "update"
    DELETE = "delete"

    @classmethod
    def _missing_key(cls, name):
        # This method is called when a name is not found in the enum
        # You can customize the behavior here, for example, allowing case insensitive access
        return getattr(cls, name.upper(), None)  # Return the enum member if it exists, otherwise return None



query1 = Query["INSERT"]  # This will return the enum member Query.INSERT
query2 = Query["Insert"]  # This will go through _missing_key and return Query.INSERT
print(query1 is query2)  # This will print True, as both refer to the same enum member

query3 = Query["insssseeerrtt"]  # This will go through _missing_key and return None
print(f"query3: {query3}")  # This will print None, as the name is not valid


Just to close this post I'll mention an interesting article that discusses inheriting from Enum vs using a new metaclass that inherits from EnumMeta.

Sunday, 6 September 2026

Python Dynamic Class Creation part 2

In a recent post I mentioned the rough steps that Python executes to create a class when it comes across a class statement (we can call it the "class creation pipeline")

  • Step 1 — Determine the metaclass. Python calls __build_class__ (a builtin), which inspects the bases and the explicit metaclass= kwarg to resolve which metaclass to use (with MRO-based metaclass conflict resolution).
  • Step 2 — Prepare the namespace. The metaclass's __prepare__ classmethod is called: namespace = Meta.__prepare__('Foo', (Base,), **kwargs). This returns the dict (or dict-like object) that will serve as the class namespace. For type, this is just a plain dict. For enum.EnumMeta, for example, it returns a special _EnumDict.
  • Step 3 — Execute the class body. The compiled code object for the class body is executed as a function, with the namespace from step 2 as its locals(). This is the key insight: it's essentially exec(body_code, globals(), namespace). After this, namespace contains {'x': 1, '__module__': ..., '__qualname__': ...}.
  • Step 4 — Call the metaclass. Meta('Foo', (Base,), namespace) is called — which, for the default type, invokes type.__call__ → type.__new__ → type.__init__. This is where the actual class object is constructed.

And explained how when we create a class dynamically using type() (or any other metaclass), we are directly at step 4. Obviously we have to take care of deciding what metaclass to use and prepare the namespace ourselves, which means that we should invoke the metaclass __prepare__ method (hook) ourselves, something that I was not aware of. Deciding what metaclass to use may seem pretty straightforward, and it is if we intend to use a particular metaclass, but if we just want to use the one that corresponds based on the inheritance chain it's not that evident. Most times, when all base classes just use type, it's just type, but we really would have to take care of analysing the metaclasses of the classes in that inheritance chain.

So indeed, correctly building a class dynamically is more complex that it seemed to me... Well, not really, cause I had missed that since python 3.3 the types module comes with a nice function just for that, types.new_class(name, bases=(), kwds=None, exec_body=None). This function follows the 4 aforementioned steps in the class creation process. It will select the metaclass to use based on whether we provide a metaclass in kwds or the parent classes in bases. It will invoke __prepare__ to create and prepopulate the class namespace, and pass it over to the exec_body function for it to continue populating it (that exec_body function corresponds to the code that we will place in the class body of a normal class statement). Finally it will invoke the selected metaclass to create the class.



def populate(namespace):
	for field in fields:
		namespace[field.name] = create_descriptor(field)

Generated = types.new_class(
	"Generated",
	(Base,),
	{"metaclass": Meta},
	populate,
)


There's another function in types that should call your attention, types.prepare_class(name, bases=(), kwds=None). It returns the metaclass to be used and the namespace created by metaclass.__prepare__, so that then you can further populate that namespace (with the code that you would put in the exec_body callback if you were using new_class), and invoke that metaclass.



meta, namespace, remaining_keywords = types.prepare_class(
	"Generated",
	(Base,),
	{"metaclass": Meta},
)

for field in fields:
	namespace[field.name] = create_descriptor(field)

Generated = meta(
	"Generated",
	(Base,),
	namespace,
	**remaining_keywords,
)


So while new_class performs the 4 steps in the class creation pipeline, prepare_class performs the 2 first steps and leaves under your responsibility performing steps 3 and 4. We could say that create_class follows a callback-based style while prepare_class moves us into an imperative, phase-oriented style.

Sometimes it can be not possible to decide what metaclass should be used based on the base classes combination (and the explicit metaclass if we happen to have provided one). If the base classes drive us to different, uncompatible (not in the same inheritance chain), metaclasses, we get an error:



>>> class MetaA(type): pass
... 
>>> class MetaB(type): pass
... 
>>> class A(metaclass=MetaA): pass
... 
>>> class B(metaclass=MetaB): pass
... 
>>> class C(A, B): pass
... 
Traceback (most recent call last):
  File "python-input-4", line 1, in module
    class C(A, B): pass
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases


If the metaclass obtained from the base classes and the one we provide explicitly do no belong to the same inheritance chain,



>>> class C(B, metaclass=A): pass
... 
Traceback (most recent call last):
  File "python-input-6", line 1, in module
    class C(B, metaclass=A): pass
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases


Saturday, 29 August 2026

Python Object Oriented Enums

Years ago, when I wrote this post about Python enums I mentioned this:

if you want something more advanced like the Java/Kotlin enums, where we can have multiple attributes as values and have instance methods (the archetypical Java Planets example), you can use the powerful aenum module (advanced enums), that just implements the Planets example.

Well, I've just found out that the standard Python enums support "enum members with associated data". You can add methods and properties to your enum classes, so you have a functionality pretty similar to the powerful Java enums. I've felt a bit like an idiot, cause after learning this from a code discussion with a GPT I realised that it's not any "python hidden feature", the standard enums documentation comes with a Planet example.


class Planet(Enum):
    MERCURY = (3.303e+23, 2.4397e6)
    VENUS   = (4.869e+24, 6.0518e6)
    EARTH   = (5.976e+24, 6.37814e6)
    MARS    = (6.421e+23, 3.3972e6)
    JUPITER = (1.9e+27,   7.1492e7)
    SATURN  = (5.688e+26, 6.0268e7)
    URANUS  = (8.686e+25, 2.5559e7)
    NEPTUNE = (1.024e+26, 2.4746e7)
    
    def __init__(self, mass, radius):
        self.mass = mass       # in kilograms
        self.radius = radius   # in meters
        
    @property
    def surface_gravity(self):
        # universal gravitational constant  (m3 kg-1 s-2)
        G = 6.67300E-11
        return G * self.mass / (self.radius * self.radius)

Planet.EARTH.value

Planet.EARTH.surface_gravity

That's the equivalent to the Java Planet one that years ago showed the world that enums could be much more than what we were used to.


public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;   // in kilograms
    private final double radius; // in meters
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
    private double mass() { return mass; }
    private double radius() { return radius; }

    // universal gravitational constant  (m3 kg-1 s-2)
    public static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }
    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }

"Traditional" enums, like those in C or C# are just "a group of named constant values with type safety", they answered this question "How do I give names to a set of integer constants?". Java enums are "classes with a fixed set of instances defined at compile time", and they answer this question "How do I represent a fixed set of domain objects?"

Before Java added enums to the language, people were using the "Typesafe Enum Pattern" pattern to simulate "object-oriented enums". By the way, years ago I wrote about emulating Java enums in C#.

Java enums have an extra feature that is not directly implemented by Python enums, each member of the enum can have its own implementation of some methods (it's a per-instance override). Let's see an example:


enum Operation {
	PLUS { public int apply(int a, int b) { return a + b; } },
	
	TIMES { public int apply(int a, int b) { return a * b; } };
	
	public abstract int apply(int a, int b);
}

Python does not have a direct equivalent, a member can’t override a method. But there are idiomatic workarounds: carry the behaviour as data (a callable in the tuple) and dispatch on self inside one method:


class Operation(enum.Enum):
	PLUS = (operator.add,)
	TIMES = (operator.mul,)

	def __init__(self, fn: Callable[[int, int], int]) -> None:
		self.fn = fn

	def apply(self, a: int, b: int) -> int:
		return self.fn(a, b)

In Python we can dynamically look up an enum member both by name Color["RED"] and by value (Color("red")). The latter is not supported in Java. Related to this, we can customize behaviour when trying to access an element using a non existing value, by means of defining a _missing_ method:


from enum import Enum
class Color(Enum):
    RED = "red"
    BLUE = "blue"
    @classmethod
    def _missing_(cls, value):
        value = value.lower()
        for member in cls:
            if member.value == value:
                return member
        return None

Color.RED is Color("rEd")
# True

Saturday, 15 August 2026

Linux Dynamic Linking

A few months ago I wrote a post about glibc, it's a good time now to follow up with a related element of linux systems, ld-linux.so, the dynamic linker or dynamic loader.

If you need a refresh on what linking is, this wikipedia article should be enough. In this post I'll be talking about dynamic linking on Linux (performed by ld-linux.so), do not confuse it with the static linking performed by the ld tool (part of the binutils package, in turn part of the build-essential metapackage) to create an executable copying inside it the library code it references.

So dynamic linking means loading into a process memory at runtime the libraries (shared objects, .so's) that it needs. This loading is mainly done when the process starts (based on the imports defined in the ELF binary file), but can also be performed dynamically at any point during the process execution, with the dlopen function. In both cases, ld-linux.so (at least in Ubuntu its exact name is: /lib64/ld-linux-x86-64.so.2) is used to perform this loading and linking.

If ld-linux.so is used to load so's, and ld-linux is a so itself, this looks like a chicken-egg problem. Well, it's the kernel itself who takes care of loading it when a process is launched:

A binary names its loader in the ELF .interp section (readelf -p .interp /bin/ls → /lib64/ld-linux-x86-64.so.2). On execve the kernel reads .interp, hands control to that interpreter, which maps the libraries and jumps into the program.

Notice that the dynamic linker/loader is also known as the ELF interpreter.

When you run a dynamically linked program, the Linux kernel reads the executable's Executable and Linkable Format (ELF) header. It looks for a specific section called .interp (or the PT_INTERP program header), which contains the hardcoded string path to this exact interpreter. How the ELF Interpreter Works: Rather than running your program directly, the operating system kernel actually loads and hands control over to ld-linux first. The interpreter then performs several crucial tasks:
- Finds Dependencies: It scans your binary to see which shared libraries (such as libc.so) it needs.
- Loads Libraries: It locates those .so files on the disk and maps them into the program's memory space.
- Resolves Symbols: It performs "relocations," fixing memory references so your program knows exactly where library functions exist in memory.
- Launches Program: Once the environment is ready, it hands control back to your program's main entry point.

There's a strong relation between glibc and ld-linux. They are built together carrying the same version:

The loader (ld.so) and libc.so.6 are a version-locked matched pair. They are built from the same source tree in the same build and talk to each other over a private, unstable ABI: the GLIBC_PRIVATE symbols.
nm -D /lib64/ld-linux-x86-64.so.2 | grep GLIBC_PRIVATE # loader DEFINES them (T/D)nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC_PRIVATE # libc has them UNDEFINED (U)

libc.so.6 lists symbols like _dl_allocate_tls@GLIBC_PRIVATE, __tunable_get_val@GLIBC_PRIVATE, _dl_find_dso_for_object@GLIBC_PRIVATE as undefined — “the loader must hand these to me at startup.” The names, semantics, and the layouts behind them (TCB/TLS, _rtld_global) change freely between glibc versions.

ld-linux.so can be executed on its own, as a normal executable. If you run it with the --help version (/lib64/ld-linux-x86-64.so.2 --help) you get this interesting information.

You have invoked 'ld.so', the program interpreter for dynamically-linked ELF programs. Usually, the program interpreter is invoked automatically when a dynamically-linked executable is started.

You may invoke the program interpreter program directly from the command line to load and run an ELF executable file; this is like executing that file itself, but always uses the program interpreter you invoked, instead of the program interpreter specified in the executable file you run. Invoking the program interpreter directly provides access to additional diagnostics, and changing the dynamic linker behavior without setting environment variables (which would be inherited by subprocesses).

libc.so also happens to have an entry point, but it seems to have no other function that showing some information about itself: (/lib/x86_64-linux-gnu/libc.so.6 --version)

GNU C Library (Ubuntu GLIBC 2.39-0ubuntu8.8) stable release version 2.39.
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 13.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Minimum supported kernel: 3.2.0

Invoking ld-linux.so with --version in the same system you'll see that it's the same version as glibc, /lib64/ld-linux-x86-64.so.2 --version

ld.so (Ubuntu GLIBC 2.39-0ubuntu8.8) stable release version 2.39.

Notice that in the glibc information you can see "Minimum supported kernel: 3.2.0". Bearing in mind that my current kernel is 6.8.0, so we can say that glibc is quite little demanding with regards to kernel evolution.

Probably the ldd tool resonates with you. It's just a bash script wrapper (/usr/bin/ldd) around ld-linux.so

Saturday, 8 August 2026

Python Function Call

Over the years I've come to appreciate a real lot how coherent the Python object model is. For example, the callable concept applies to every object that can be invoked, starting by functions. When you define a function, you have an object that is an instance of the types.FunctionType class, a class that has a __call__ method. In contrast, Kotlin has the invokable concept, but this is an extra (the invoke operator) that you can add to your classes, but the basic invokable element, a method/function, is not an instance of a class with an invoke operator. Functions/methods in Kotlin (at the Kotlin language level, then at a JVM or JavaScript runtime things differ) are not objects, we have to get a function reference or a method reference (with the :: syntax) to treat them as objects.

So having in mind that the callable concept applies to normal functions and methods has made me to dive deeper into what I explained in a recent post:

At the C level, all Python objects are represented by the PyObject structure, which points to a PyTypeObject (its type). The type object defines how instances of that type behave.

To make calls fast and avoid creating temporary tuple/dict arguments, CPython uses the vectorcall protocol (Py_TPFLAGS_HAVE_VECTORCALL).
When the interpreter encounters a CALL opcode, it ultimately looks at the target object's type to find its vectorcall entry point:

- Pure Python Functions (PyFunction_Type): The vectorcall pointer points to _PyFunction_Vectorcall. This function extracts the function's PyCodeObject (func->func_code), allocates a new _PyInterpreterFrame on the evaluation stack, and pushes it to _PyEval_EvalFrameDefault.

- Built-in/Native Functions (PyCFunction_Type): Native functions (like math.sqrt or print) are wrapped in a PyCFunctionObject. Their vectorcall pointer points to _PyCFunction_Vectorcall. This function extracts the underlying C function pointer (func->m_ml->ml_meth) and invokes the compiled C code directly.

But, based on the "callable protocol", when the interpreter encounters a CALL opcode, it should check if the type of the object has a __call__ method and invoke it, right? How does this fit with the above? Well, we have to separate language semantics from implementation:

It's a classic example of language semantics vs. implementation details:

At the language level (Python semantics): Everything is an object, and every object that can be invoked implements the callable protocol via __call__. Defining __call__ on function and builtin_function_or_method keeps the object model clean, predictable, and fully inspectable.

At the runtime level (C implementation): The interpreter is allowed to use any shortcut it wants as long as it doesn't break the language semantics. Since invoking fn() is the most critical operation in the language, CPython uses type-checking fast paths (ob_type) to skip the overhead of attribute lookups.

Going deeper:

Looking up __call__ via the attribute resolution mechanism on every function call would carry massive performance overhead. Here is how CPython handles this under the hood and how it fits with the previous explanation. 1. Fast Paths Bypass __call__ Entirely

For pure Python functions (types.FunctionType) and standard C builtins (types.BuiltinFunctionType), CPython completely bypasses the __call__ method lookup.

Even though type(func).__call__ exists at the Python language level (defined on function / PyFunction_Type), the bytecode interpreter does not execute a dictionary lookup for __call__ when you write func().

Instead:

The interpreter checks Py_TYPE(callable).

If it matches &PyFunction_Type or &PyCFunction_Type, it executes the direct fast paths explained previously (allocating a _PyInterpreterFrame or invoking the vectorcall C function pointer directly).

As for custom callables (instances of classes with a __call__ method), the mechanism is also optimized for performance (it's not doing an expensive MRO __call__ search for any object). The structure representing and object's type, PyTypeObject, has 2 fields, tp_call and tp_vectorcall_offset serving different generations of CPython's calling API. tp_vectorcall_offset is used among others for bound methods and functools.partial. tp_call is used for classes that define a __call__ method (and their derived classes). In that case tp_call points to the generic C wrapper that performs the lookup for "__call__" in the object's class dictionary/MRO and invokes it (and so, if the object does not have a tp_call no search is performed at all).

If we have a function: def fn(), we can invoke it normally, via fn(), but also like this: fn.__call__(). The latter invokation form ends up creating the corresponding _PyInterpreterFrame and running fn's code_object in _PyEval_EvalFrameDefault(), but does it through quite a few extra steps. First we have the look up of the __call__ attribute in the fn object, which will find it in the Function class (types.FunctionType). Retrieving it will give us a bound method (types.MethodType), where __self__ is the fn function, and __func__ is the __call__ function. Then, we have the normal invokation of that bound method, that will end up executing the code object for the fn function.

Thursday, 30 July 2026

AsyncLoop 2026

With "async loop" in this post I'm not talking about something like JavaScript's for-await or Python's async-for, where the iterator is asynchronous. What I'm talking about here is about performing an asynchronous operation in a loop (like doing a loop of http requests). Well, in the async/away world that's not something to be scared of, but in the past, when asynchronous code was based on callbacks (not even Promises) this was a bit more complicated. 15 years ago I published a couple of posts about that [1] and [2]. For whatever the reason recently my mind came to think a bit about that old stuff, and I've decided to do a sort of generic AsyncLoop class that could be used with callback based functions. It's intended for use as a sort of for-of loop (so for iterator based loops) and for normal for loops (with a specific logic for getting the next item and checking for stop).

The AsyncLoop class receives a function to obtain the next item in the iteration, a function to check if the iteration must finish and a function (actionFn) for the body of the loop. That function is the one that behaves asynchronously and expects a callback. The class provides to the action/body a callback that will take care of continuing (or stopping) with the next iteration.


class AsyncLoop {
    constructor(nextItemFn, conditionFn, actionFn, onEndFn) {
        this.nextItemFn = nextItemFn; // function that returns the next item
        this.conditionFn = conditionFn; // function that receives the current item 
        this.actionFn = actionFn; // function that receives the current item, a callback to invoke when done
        this.onEndFn = onEndFn; // function that is invoked when the loop ends
    }

    run() {
        let item = this.nextItemFn();
        if (this.conditionFn(item)) {
            this.actionFn(item, () => {
                    this.run();
                }
            );
        } 
        else {
            this.onEndFn();
        }
    }
}


We can use it like this:


// callback based asynchronous function
function getContent(item, callback) {
    setTimeout(() => {
        let content = "Content for: " + item;
        callback(content);
    }, 500);
}

function testAsyncLoop() {
    let countriesIter = ["France", "Germany", "Italy"].values();
    new AsyncLoop(
        () => countriesIter.next().value, // nextItemFn
        (item) => item !== undefined, // conditionFn
        // actionFn
        (item, nextFn) => {
            getContent(item, result => {
                console.log(result);
                nextFn();
            }); 
        },
        () => console.log("All items processed") // onEndFn
    ).run();
}

We can use it also in nested loops:


class Continent {
    constructor(name, countries) {
        this.name = name;
        this.countries = countries;
    }   
}

function testNestedAsyncLoop() {
    let continentsIter = [
        new Continent("Europe", ["France", "Germany", "Italy"]),
        new Continent("Asia", ["China", "Japan", "India"]),
        new Continent("Africa", ["Nigeria", "Egypt", "South Africa"])
    ].values();

    //nested loop
    new AsyncLoop(
        () => continentsIter.next().value, // nextItemFn
        (continent) => continent !== undefined, // conditionFn
        (continent, nextContinentFn) => {
            console.log("Processing continent: " + continent.name);
            let countriesIter = continent.countries.values();
            new AsyncLoop(
                () => countriesIter.next().value,
                (country) => country !== undefined,
                (country, nextCountryFn) => {
                    getContent(country, result => {
                        console.log(result);
                        nextCountryFn();
                    });
                },
                () => nextContinentFn() // onEndFn
            ).run();
        },
        () => console.log("All continents processed")
    ).run();
}

Async/await has always felt a bit like magic (all what the compiler does under the covers), but looking to the above code that we would have written in 2011 and comparing it to how we would write it now with async/await is like a quantum leap!


function getContentPromisified(item) {
    return new Promise((res, rej) => getContent(item, res));
}

async function testUsingAsyncAwait(){
    let continents = [
        new Continent("Europe", ["France", "Germany", "Italy"]),
        new Continent("Asia", ["China", "Japan", "India"]),
        new Continent("Africa", ["Nigeria", "Egypt", "South Africa"])
    ];
    for (let continent of continents) {
        console.log("Processing continent: " + continent.name);
        for (let country of continent.countries) {  
            console.log(await getContentPromisified(country));  
        }
    }
    console.log("All continents processed");
}