I can’t help but come back and talk about some of my favorite topics. TypeScript, C#, Python and Pydantic. Pydantic is a fantastic library for Python that helps you make your Python code more safe and structured, more TypeScript-like.

I am quite used to write React applications with TypeScript. TypeScript is a great language for writing browser based JavaScript clients. It helps you write JavaScript code with similar type safety as you can get with C#.

Why type safety?

Type safety means that when you’re making a type error while coding, for example pushing an object of the wrong type as an argument to a method, the editor warns you already in the coding phase and prevents you from making the time-costly mistake.

A real world analogy could be a vending machine that accepts only €1 coins. You want that machine to prevent you from inserting five cents instead of letting you jam it with wrong coins.

Why C# is the better type safe language

There is a lot of similarity between C# and TypeScript languages. However, it appears to me that C# is the “better” TypeScript.

Despite of all the benefits that TypeScript provides over standard JavaScript, there is no getting around the fact that TypeSCript is rather a toy language - it does not have its own compiled byte code. Instead, TypeScript is transpiled to JavaScript, which is another interpretable language. Compared to JavaScript, C# executes up to 50 times faster, depending on the task and the environment.

So TypeScript is just a layer on top of the real underlying language, and it is very slow. If you want to have real programming language with a great similarity to TypeScript, I’d recommend to have a look at C# language.

Let’s compare an implementation of a Person class in both languages. First, the TypeScript variant:

class Person {
  // Read-only property
  public readonly id: number;

  // Public field
  public name: string;

  // Getter (computed)
  public get hasEvenId(): boolean {
    return this.id % 2 === 0;
  }

  // Constructor
  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

// Example usage:
const p = new Person(42, "Alice");
console.log(`${p.name}, even ID? ${p.hasEvenId}`);

And then, the same thing in C#:

person class

public class Person
{
    public int Id { get; }

    public string Name { get; set; }

    public bool HasEvenId => Id % 2 == 0;

    public Person(int id, string name)
    {
        Id = id;
        Name = name;
    }
}

Example usage:


var p = new Person(42, "Alice");
System.Console.WriteLine($"{p.Name}, even ID? {p.HasEvenId}");

Obviously, there are syntax differences. But conceptually they are the same. They are statically typed. They are (de-facto) strongly typed. They are object oriented. I claim that any seasoned TypeScript programmer can become an efficient C# programmer in one or two days.

Why use TypeScript in the backend?

The use of TypeScript has been traditionally justified because that’s the best language that you can execute in the browser (as JavaScript). Further, TypeScript has also been conquering the server side. It is being often used to write backend code as well.

The reasoning why TypeScript is often written for the backend is consistency. Since the user interface must be implemented with TypeScript anyway, isn’t it great that the server-side code is also written in the same language?

Then it is easier for the UI developer to occasionally make corrections to the backend and vice versa. If one full stack developer works on both backend and frontend, it can lower the context switch threshold.

Then why not use C# instead of TypeScript?

if the whole point of writing the backend code with TypeScript is consistency with the user interface code, why not use C# entirely for this? Namely, with the arrival of WebAssembly, TypeScript is no longer the only language you can use to write type-safe code to run in a browser. Just check out Blazor!

C# might win long term

Compared with TypeScript, I believe that C# could win the race in long term. TypeScript has been recently gaining popularity. The reason for this, I believe, is because it provides some artificial respiration for its underlying old language JavaScript, which has been widely used just because there were no alternatives. Based on C#’s fundamental advantage - (it’s faster and also corporate-backed) - and the fact that it now can be run in browsers, I believe that TypeScript’s spike in popularity is short-term whereas C# will continue its steady growth trajectory. Of course I might also be wrong. Let us see what happens!

When type safety is bad

Let’s turn everything upside down now. Statically typed “safe” languages can also be really annoying at times. Mandatory typing can make it slower to write simple code. Sometimes you want to prototype something fast to experiment with your ideas. If you are forced to write verbose code with all the whistles and bells, it can be quite frustrating when you just want to try out your idea as simply as possible. In contrast to statically typed “type safe” languages, there are dynamically typed “scripting languages” such as Python and Ruby.

My journey with Python and Ruby

Personally, I became interested in Ruby language around 2006. I wanted to find an alternative to Java that would allow me to easily prototype ideas on my machine, a bit similar to Perl or Bash scripts but object oriented. I was struggling with the decision whether to choose Python or Ruby. I went for Ruby. Ruby was trending that time because of Ruby on Rails web framework, which was a great alternative to Java based web apps. I even ended up doing some customer projects with Ruby.

Years later, around 2023 or so, I had to let Ruby go with a heavy heart. I had come to admit that Ruby had lost a big part of its ancient popularity over the years. On the other hand, Python had become a superior language for which you could find a huge number of libraries and examples.

I thought it would be hard for me to rewire myself to adopt Python after so many years of exposure to Ruby. However, it turned out that Python was amazingly simple to learn. In many ways it appeared like simplified Ruby.

In deed, Ruby’s versatility allows the same code to be implemented in multiple ways. It is powerful but it also increases the risk of so-called “WTF” moments. Sometimes you might look at someone else’s Ruby code and wonder if this is Ruby at all. Maybe this is the ultimate reason for Ruby’s decline.

Fixing Python with Pydantic

I started using Python for a lot of tongue-in-cheek hobby tasks. However, I wasn’t entirely happy with Python either. I realized that although dynamic typing allows me to write code faster, it also makes me stumble faster. If you have to implement more complex objects and pass them around in your software, it is often hard to go back after a break and continue developing the old code base when you have already partially forgotten the original logic behind the code. I figured out that it’s possible to largely tackle this issue by adding type checking with Pydantic library!

Data validation

Data validation inside the code structures, I believe, is a close cousin to unit tests. Writing classes for your data structures and using Pydantic or similar libraries, preferrably with MyPy type checking, can actually equip you with a far more integrated safety than just writing unit tests. Moreover, unit tests are always like a separate thing. They are not part of the actual code, but rather like shadow code for the actual functional stuff. That may be part of the reason why many developers still feel awkward with unit tests. Embedding data validation gives similar safety to your code as you get with unit tests, but with far more pleasant developer experience. These two things can surely complete each other.

With data validation, you can actually get more type safety than with classic static type checking. I would even claim that doing proper data validation embedded in the actual function code can reduce reliance on unit tests to some extent. Data validation as code quality mechanism provides advantages that cannot be achieved with unit tests only. As data validation is embedded in the code structure itself, it can be more intuitively updated by the developers when the code changes.

How about unit tests?

Run-time data validation inside the executable business code supported by core unit tests may be a more balanced alternative for long term productivity and maintainability than unit tests alone or sole reliance on run-time data validation.

If I code privately and personally to learn and have fun,I tend to write lots of data validation with Pydantic whereas only writing only very slight layer of unit tests.

I equip those places with extensive validation that I know I might be struggling to understand later. My fingers would literally start aching if I had to write extensive tests covering all imaginable failure scenarios for all those functions that I will trash in a day or two anyway. When I write code that is only meant for myself, I usually know which places will be hard to comprehend later. I will go sparingly and add extra clarity primarily those hot spots. This approach has evolved over time to maximize personal productivity. Go smart instead of being dogmatic out of the desire just to follow some principle.

To put it short, instead of writing an extensive amount of unit tests for all possible failure cases only to be wiped out just days or hours later when the actual function changes, I tend to keep the unit test layer as light weight as possible. The unit tests have more of a documentary role in this approach - serving as an example how to the function is intended to be used. In this approach, data validation inside the runnable code is the actual workhorse for code quality and readability. Since the validation layer is deeply embedded inside the code structure itself, it also feels more intuitive to update the the validation part when the code changes, compared to updating unit tests which are always somewhat external to the actual executable code.

Does this work in corporate projects?

I just described my very personal “hobby” workflow that is intended to personal projects that have emphasis on learning things and trying out something new. This is of course different from corporate projects, when you are coding with others and for others. Then, unit tests matter a lot. That’s a different world. In corporate projects, communication matters a lot. There are meetings, discussions, requirements, reviews, processes, doctrines for code coverage etc. You walk together, you walk slow but you walk far and so on and so on. When you create code that is meant to be understood and maintained by someone else, it is harder to estimate which parts of the code may be hard to comprehend by the others.

Therefore the approach to selectively apply more security (by run-time data validation or unit tests) to places where you think that you would need does not work that well. After all, you can’t know how others see your code without communicating it with them. Therefore it is more important to walk together the path of code reviews and discussions. If someone reviews your code, you should always have all the patience it takes to discuss and change the code until everybody is satisfied. It’s also a great way to learn new things!

Validation example

To give a practical example about improving maintainability with validation, let’s have a detailed look. For example, you want to write a function that is meant to that takes in a string parameter. In this case, the string is expected to be in a specific date format, having a certain string syntax:

def get_year(date: str) -> str:
    return date.split("-")[0]

If you use a statically typed language to write this function, it won’t do anything to prevent you from passing a string that is in a wrong format. Ideally you don’t want to clutter this clean function with a logic to check that the date string is in the right format. So let’s use Pydantic to write a DateString that will safeguard that the function will never get an unsuitable string:

from pydantic import BaseModel, field_validator
from datetime import

class DateString(BaseModel):
    value: str

    @field_validator('value')
    def validate_date_format(cls, value: str) -> str:

        # Check if the string matches the "YYYY-MM-DD" format
        try:
            datetime.strptime(value, "%Y-%m-%d")
        except ValueError:
            raise ValueError('Date must be in the format "YYYY-MM-DD"')

        return value

Of course, you can write unit tests for DateString to make sure it works! And now, let’s modify the original function:

def get_year(date: DateString) -> str:
    return date.value.split("-")[0]

Now, get_year function can breathe again - it can trust that date is in correct format. Not convinced yet? Let’s have a another practical example on how to make your code more robust with Pydantic data validation.

String processing flow in Pydantic

Le’ts assume that you are doing some processing to your files with Python. For example, we want to process blog post files with Python. First of all, let’s write a Pydantic class to validate that blog article’s full file name is in the correct format:

import re

class FullFilename(BaseModel):
    value: str
    @field_validator("value")
    def validate_value(cls, v: str) -> str:
        pattern = r'^(\d{4})-(\d{2})-(\d{2})-(.+)\.md$'
        match = re.match(pattern, v)
        if not match:
            raise ValueError(
                "Name must match the format: 'YYYY-MM-DD-filename-here.md'"
            )
        return v

Now, let’s instantiate the object:

full_file_name = FullFilename(value='2025-01-22-turning-python-into-your-typescript-replacement.md')

Now we have wrapped this blog posts’s file name inside a Pydantic model. If the file name is in the wrong format, the code’s execution will stop and an error is thrown. To understand how to benefit from wrapping this single string inside a validation model, let’s code on.

If we actually want to read that blog article into memory so we can programmatically modify it, in deed we really need its exact path. Assuming that there is a fixed storage folder in your system for blog articles let’s make an insanely useful FullPath model that can be derived from a FullFilename object:

class FullPath(BaseModel):
    value: str

    @classmethod
    def from_full_filename(cls, full_filename: FullFilename):
        name = full_filename.value
        path = f'/home/metamatic/workspaces/metamatic-blog/{name}'
        return cls(value=path)

Further, to play around with your blog article like a true juggler in the circus, you can use FullPath to instantiate the article:

class Article(BaseModel):
    path: FullPath
    text: Optional[str]

    @classmethod
    def from_path(cls, path: FullPath) -> 'Article':
        return cls(path=path, text=None)

Just go on to populate the text in the article by adding get_text method. Let’s make the method function lazily. It returns the blog text if it’s already loaded. If not, only then load it.
You can actually load the text because the article surely holds a FullPath object that knows exactly where the text file is located:

def get_text(self) -> str:
    if self.text is None:
        import File # why would you import a library before you need it!
        self.text = File.read(self.path.value)
    return self.text

This may appear boring at the first glance - but just imagine all those things that you can do when you have the blog text in the memory… You can programmatically modify it as you wish! You can decorate it with pictures, voice narrations… Only your imagination is the limit!

For my (shady) purposes (that I cannot reveal), I might even need a version of this file name that does not contain the file extension “.md”:

class ExtensionlessFilename(BaseModel):
    value: str

    @classmethod
    def from_full_filename(cls, data: FullFilename) -> 'ExtensionlessFilename':
        full_filename: str = data.value
        extensionless_filename = full_filename.split(".")[0]
        return cls(value=extensionless_filename)

And then, I instantiate an extensionless filename object using this model:

extensionless_filename = ExtensionlessFilename.from_full_filename(value=full_file_name)

This instantiates variable extensionless_filename as an instance of the model ExtensionlessFilename, which guarantees that the string value it contains is the right stuff, blog article’s full filename without file extension.

Instead of simply accepting a string parameter, constructor from_full_filename method requires an instance of FullFilename class as parameter. Since FullFilename class is known to have already validated its stuff, ExtensionlessFilename class can safely assume that the filename string is in the right format and we don’t need to do additional checks at this point anymore. So we can safely split the contained string by “.” delimiter. There is no need to fear that the function will blow up due to a missing “.” character.

Passing strings inside validator wrappers instead of just using them in their plain form can be quite useful long term. It’s a bit same thing as in real life. For example, if you are walking around at workplace and want to ask a colleague something, you can go talk to them without needing to require them to show their ID first. That’s because it is known that only people who have a company badge can enter the building. It’s kind of the same thing with these validator classes.

My experience is that this approach can really make the code more reliable, reducing uncertainty quite a bit. I would even say that it makes writing Python code at least as sturdy as with TypeScript and C#.

Cheers!