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"\x0A"
            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