Blog

  • Hello World!

    This blog will have a number of items that I write about including:

    1. Personal updates
    2. Cool things I’ve learned in the tech sphere
    3. Conference recaps
    4. Hobby deep dives
    5. And more!

    For now, we’ll keep it light and just have this in place for now while content is built out. Have a good one y’all!

  • pytest Features That Changed How I Write Tests

    pytest Features That Changed How I Write Tests

    This week for my AST Analyzer project I decided to do a deep dive on the testing framework pytest. When i started off the week I genuinely thought I already knew everything I needed, but man was I pleasantly surprised at how powerful that testing suite is. Today I’m going to go over a few new things I learned when implementing my tests suite, including:

    1. Assertion behind the scenes
    2. Fixtures
    3. Capsys and Caplog
    4. Parametrization

    Like last week’s post, most of these were learned with the combination of two resources:

    Assertion

    One of the first things I noticed when switching from unittest to pytest was how much simpler assertions are. In unittest, you need to use specific methods like assertEqual, assertTrue, assertIn, etc:

    # unittest style
    self.assertEqual(result, 5)
    self.assertTrue(is_valid)
    self.assertIn("error", message)
    

    With pytest, you just use Python’s built-in assert statement:

    # pytest style
    assert result == 5
    assert is_valid
    assert "error" in message
    

    I was wondering how a 3rd party package could make assertion statements clearer than what’s built in to Python, and while digging deeper I found that there’s a whole process that goes on when the test fails.

    Assert Rewriting with AST

    Pytest uses the built in assert function because it raises an AssertionException when the case does not pass. This built-in assertion check allows Pytest to work with built-in functionality and expand functionality from there.

    For example, once an AssertionException is raised pytest uses assertion rewriting to replace the default string. This is done at import time where pytest can grab the value of each variable and sub expressions. This new info is used to provide the logs with more details like:

    1. Provides a diff of what was given and what was expected
    2. The line the test failed at
    3. What was the failure (mismatch, missing item, etc)

    By doing this at import time, it’s able to place all code in an Abstract Syntax Tree (yes, the same AST we’re working with in this project), finds all the assert statements, and rewrites them to capture intermediate values before the assertion runs. This is why when an assertion fails, pytest can show you exactly what each part of the expression evaluated to:

    def test_string_comparison():
        result = "hello world"
        assert result == "hello pytest"
    
    =========================== FAILURES ===========================
    _________________ test_string_comparison _______________________
    
        def test_string_comparison():
            result = "hello world"
    >       assert result == "hello pytest"
    E       AssertionError: assert 'hello world' == 'hello pytest'
    E         
    E         - hello pytest
    E         + hello world
    
    ======================== 1 failed in 0.02s =====================
    

    This works for complex expressions too:

    def test_list_membership():
        items = ["apple", "banana", "cherry"]
        target = "grape"
        assert target in items
    
    E       AssertionError: assert 'grape' in ['apple', 'banana', 'cherry']
    

    Pytest captured both the value of target and items before the assertion ran, so it can show you exactly why it failed.

    Fixtures

    Coming from Node, one thing I appreciated about jest is being able to use beforeEach, beforeAll, afterEach, and afterAll in combination with describe scopes to setup and tear down test data in a straightforward manner. I was relieved to see that pytest gives us these features with the ability of fixtures that allow us to specify the same level of setup, teardown, and scope that’s in jest.

    Why Fixtures over beforeEach

    While I was relieved to see familiar patterns, I quickly realized that fixtures are actually more powerful than Jest’s approach in a few key ways:

    • Composition – Fixtures can depend on other fixtures. We injected pytest’s built-in tmp_path fixture into our own custom fixture – that’s composition in action. In Jest, you’d have to nest your beforeEach blocks or manually call setup functions to achieve something similar.
    • Scoping – Fixtures let you specify how often they run: function (default, runs for each test), class, module, or session. If I have an expensive setup like connecting to a database, I can scope it to session and it only runs once for the entire suite.
    • Reusability via conftest.py – Any fixture defined in a conftest.py file is automatically available to all tests in that directory and subdirectories. No imports needed.
    • Pay for what you use – This one was subtle but important. With Jest’s beforeEach, the setup runs before every test in that scope whether you need it or not. With pytest, fixtures only run when a test actually requests them as a parameter. If I have 10 tests in a class but only 3 need the sample_code_file fixture, it only gets created 3 times. This keeps tests fast and avoids unnecessary setup.

    Setup and Teardown

    Part of testing the ASTAnalyzer was making sure that we had a file with data to parse throughout out tests. Initially I created a sample file in a tests/data document and tested against that, but then found that we can create one using a combination of our own fixture and one of pytest’s built in fixtures:

    @pytest.fixture
    def sample_code_file(tmp_path):
        """Factory fixture to create temporary Python files with specified content."""
    
        def _create_file(content, filename="test_file.py"):
            file_path = tmp_path / filename
            file_path.write_text(content)
            return str(file_path)
    
        return _create_file
    
    def test_enter_opens_file(self, sample_code_file):
        """__enter__ opens the file and returns file object."""
        filepath = sample_code_file("x = 1")
        with Parser(filepath) as f:
            assert f is not None
            assert not f.closed
    
    def test_context_manager_with_exception(self, sample_code_file):
        """File is closed even when exception occurs."""
        filepath = sample_code_file("content")
        file_ref = None
        with pytest.raises(ValueError):
            with Parser(filepath) as f:
                file_ref = f
                raise ValueError("test error")
        assert file_ref.closed

    As we can see, we’ve created a sample_code_file that takes in the content that we want to test against and automatically writes it to a file. This allows us to have a piece of reusable code that can be used to test all types of content inside of the file

    Capsys and Caplog

    One of the harder things I found while setting up my initial tests was making sure that my printing and logging decorators were being tested. Initially I was using unittest.patch to mimic this behavior like so:

    @logger(logging.DEBUG)
    def add(a, b):
        return a + b
    
    
    def test_ast_log_defaults():
        with patch("ast_analyzer.decorators.logger.logger") as mock_logger:
            add(3, 5)
    
        mock_logger.debug.assert_called()
        call_args = str(mock_logger.debug.call_args)
        assert "add" in call_args

    The test technically works, it checks that mock logger was called in the add() function, but it doesn’t actually check what the contents of that log are. With pytest we can use caplog to grab the output and save it to a file that we can then read from:

    @logger(logging.DEBUG)
    def add(a, b):
        return a + b
    
    def test_ast_log_defaults(caplog):
        with caplog.at_level(logging.DEBUG):
            add(3, 5)
    
        assert "DEBUG" in caplog.text
        assert "add" in caplog.text

    With this new functionality, we can look for specific strings of text inside of the log. We also have control over the logging level that we display, so we can write tests for DEBUG and INFO to check that the text in both of those logs are appearing properly. This can also be done with capsys for checking print statements:

    def test_prints_timing_output(capsys):
        """Decorator should print timing information to stdout."""
    
        factorial(3)
        captured = capsys.readouterr()
        assert "factorial" in captured.out
        assert "->" in captured.out

    In both formats we can see the ease of use that pytest gives us for accessing this text

    Parametrization

    Last but not least, there’s parametrization. Parametrization is a technique that’s used in python to make code more modular and reusable. In our specific case, we use parametrization for testing by iterating through multiple parameters. This is a great way to reduce the amount of code in your test suite without losing any functionality. To demo this, I’ll show a before and after of a series of tests made in the repo.

    Before Parametrization

    When we create our custom ASTNode, one thing we do on initialization is see how many children the Node has. In order to test that our __init__ declaration for children is working, we set up the following tests:

    def test_str_shows_children_count_one_child(self):
        """__str__ displays the number of children."""
        tree = ast.parse("x = 1")
        node = ASTNode(tree)
        assert str(node) == f"AST Node | Children: 1"
    
    def test_str_shows_children_count_mult_children(self):
        """__str__ displays the number of children."""
        tree = ast.parse("x = 1\ny = 2\nz = 3")
        node = ASTNode(tree)
        assert str(node) == f"AST Node | Children: 3"
    
    ---
    
    ================ test session starts ================
    platform darwin -- Python 3.12.12, pytest-9.0.2, pluggy-1.6.0
    rootdir: /Users/davidmoran/Sites/ai-bootcamp/projects/AST-Analyzer
    configfile: pyproject.toml
    plugins: cov-7.0.0
    collected 2 items                                   
    
    tests/test_astnode.py ..                      [100%]
    
    ================= 2 passed in 0.03s =================

    As you can see, the code is simple enough where having it be exactly the same isnt too bad, but its repetitive which is something that we want to avoid. When playing around with this I thought of combining this into a collection and testing that way:

    def test_str_shows_children_count(self):
        """__str__ displays the number of children."""
        tree = ast.parse("x = 1")
        node = ASTNode(tree)
        assert str(node) == "AST Node | Children: 1"
    
        tree = ast.parse("x = 1\ny = 2\nz = 3")
        node = ASTNode(tree)
        assert str(node) == "AST Node | Children: 3"
    
    ---
    
    ================ test session starts =================
    platform darwin -- Python 3.12.12, pytest-9.0.2, pluggy-1.6.0
    rootdir: /Users/davidmoran/Sites/ai-bootcamp/projects/AST-Analyzer
    configfile: pyproject.toml
    plugins: cov-7.0.0
    collected 1 item                                     
    
    tests/test_astnode.py .                        [100%]
    
    ================= 1 passed in 0.02s ==================

    While this is definitely a lot cleaner and lean, one thing I didn’t appreciate is that I’m stuffing two test cases into one. In the future if one of these were to fail (in a larger test) it would be a bit annoying trying to figure out which one of these was the culprit

    Introducing: Parametrization

    Parametrization solves the above by creating a matrix of key values to test against and running them against 1 test. Unlike our second option above, using parametrization breaks the singular test out into the number of suites we specified above so that we can see which of the items failed a test

    @pytest.mark.parametrize(
        "code,expected_count",
        [
            ("x = 1", 1),
            ("x = 1\ny = 2\nz = 3", 3),
        ],
    )
    def test_str_shows_children_count(self, code, expected_count):
        """__str__ displays the number of children."""
        tree = ast.parse(code)
        node = ASTNode(tree)
        assert str(node) == f"AST Node | Children: {expected_count}"
    
    ---
    
    ================ test session starts =================
    platform darwin -- Python 3.12.12, pytest-9.0.2, pluggy-1.6.0
    rootdir: /Users/davidmoran/Sites/ai-bootcamp/projects/AST-Analyzer
    configfile: pyproject.toml
    plugins: cov-7.0.0
    collected 2 items                                    
    
    tests/test_astnode.py ..                       [100%]
    
    ================= 2 passed in 0.05s ==================


    The way this works is pretty straightforward:

    1. We use the @pytest.mark.parametrize and pass in two arguments
      • A string of variable names separated by commas
      • A tuple of values that you want each variable to represent on iteration
    2. For each tuple in our collection we passed, the test will iterate over and replace the variables with the values we provided
    3. Every time the test is run, it reports it as a separate test, meaning that we can get more insight into which parameter will fail in case of an error.

    This feature of pytest works great when you want to test one specific thing against a number of start points. For our test, we just wanted to make sure that an ASTNode was created successfully based on the input, so it’s a perfect candidate for parametrization. If I wanted to check the outputs or error handling of certain inputs, that is better handled as a separate test so that we can check on a number of items (type of exception if raised, state of ASTNode, log statements, etc).

  • Python Closures: Coming from JavaScript

    Python Closures: Coming from JavaScript

    Every time I want to learn something new I always end up doing it the old school way: picking up a book. Sure, in today’s age most of the up-to-date information can be found in a mix of documentation and Udemy courses, but there’s something about picking up a piece of text (digital or hardbound) that makes me think that the knowledge there is meant to last much longer than what’s shown in a video course. I’ve always seen it as:

    1. Books – Gain depth of knowledge on a topic
    2. Videos – Quickly ramp up to use a specific technology

    That being said, one of my goals for this year is to up my Python skills so that I can be more prepared to build out items for AI/ML use cases. Part of this learning process has been getting to understand Closures in Python. At first I just saw these as a function with a hidden variable (which is technically true), but after doing some digging into what they do and how their scopes are defined I got to learn a bit more about the Python language.

    A quick heads up, most of this knowledge was gained from reading Fluent Python, 2nd Ed and Python Cookbook, 3rd Ed. If you haven’t read them already I highly recommend as they provide an in-depth explanation of Python best practices while not being too boring like a text book

    Why Closures Matter

    Before we dive under the hood, here’s a quick rundown on why closures are an important part of Python to understand early on:

    1. Decorators – Python decorators are built on closures (more on this in my next post)
    2. Callbacks – Similar to JavaScript callbacks, closures let you “remember” state
    3. Factory functions – Create specialized functions on the fly (like our multiplier example)
    4. Data encapsulation – Hide internal state without classes

    If you’ve used decorators like @app.route() in Flask, you’ve used closures—even if you didn’t realize it.

    What is a Closure?

    Closure: a nested function that remembers and accesses variables from its enclosing (outer) function’s scope, even after the outer function has finished executing, allowing the inner function to maintain state

    Looking at the definition above, you can see where I was half correct with my initial assumption. Broken down in code, we can take a look at a function like. Using blueprint code, we can start defining our closure like so:

    Basic Example: Multiplier

    Example 1: mult_closure.py: A closure to make a multiplier out of any function

    def make_multiplier_of(n):
        """Outer function that takes a factor 'n' and returns a closure."""
    
        def multiplier(x):
            """Inner function (closure) that uses the remembered factor 'n'."""
            return x * n
    
        return multiplier
    
    
    """Create two closures, each remembering a different 'n' value"""
    doubler = make_multiplier_of(2)  # Remembers n=2
    tripler = make_multiplier_of(3)  # Remembers n=3
    
    """Results. Note that we no longer need to pass a scalar as the closure will remember the initial value it was passed"""
    print(f"8 times 2 is {doubler(8)}")
    print(f"4 times 3 is {tripler(4)}")
    
    -----
    
    8 times 2 is 16
    4 times 3 is 12

    As we can see above, we’ve created a new closure that is able to create a multiplier dynamically by saving one of the scalars to memory. This allows developers to only pass through the number they want to see multiplied by instead of always passing in the scalar

    Behind the scenes, we’re returning the nested function (the closure) and allowing it to be run right after the multiplier creator is called. Another way to visualize this is side by side:

    """Saved to a variable"""
    doubler = make_multiplier_of(2)
    doubler(4)
    
    """Executed in-place"""
    make_multiplier_of(2)(4)

    JavaScript Equivalent

    Coming from JS I was curious to see if there’s an equivalent structure to this. Turns out there is!

    Example 2. mult_closure.js: The same multiplier closure from above but in JS

    function makeMultiplierOf(n) {
        // Outer function that takes a factor 'n' and returns a closure
        
        function multiplier(x) {
            // Inner function (closure) that uses the remembered factor 'n'
            return x * n;
        }
        
        return multiplier;
    }
    
    // Instances of our new closure
    const doubler = makeMultiplierOf(2);
    const tripler = makeMultiplierOf(3);
    
    // Results - the closure remembers the initial value it was passed
    console.log(`8 times 2 is ${doubler(8)}`);
    console.log(`4 times 3 is ${tripler(4)}`);

    A Real-Life Implementation

    I mentioned before that one of the things a closure can do is maintain state across calls. This can be better visualized if we built a series of log lists:

    Example 3. logger_closure.py: Saving log messages to lists in the outer function

    def create_logger(source):
        """Outer function now contains a list of logs"""
        logs = []
    
        def log_message(message=None):
            """
            Inner function, holds the logic that will be run during the call to the outer function.
            We'll always return the logs, and only append when a message is provided
            """
            if message:
                logs.append({"source": source, "message": message})
            return logs
    
        return log_message
    
    
    error_log = create_logger("error")
    info_log = create_logger("info")
    
    info_log("Hello world")
    error_log("File Not Found")
    
    print(error_log("Zero Division"))
    print(info_log())
    
    -----
    
    [{'source': 'error', 'message': 'File Not Found'}, {'source': 'error', 'message': 'Zero Division'}]
    [{'source': 'info', 'message': 'Hello world'}]

    In this iteration, we can now see that we have a new variable logs that is a list being saved in the outside function. This logs variable is saved to the scope of each creation of the closer, meaning that even though info_log and error_log both contain the list variable, they’re maintaining their own unique version of it (as seen in the print statements)

    Understanding Scope: The LEGB Rule

    Python’s way of dealing with variables is different than JS. Thing pre-ES6 days where we only had the var keyword and we had to be real careful with how we declared our variables. Nowadays those things are resolved by let and const, but Python does not have an equivalent. Instead, it assumes you know the LEGB rule and are applying it with your code. For a quick refresher:

    Example 4. legb_overview.py: Visual representation of the LEGB rule

    """Global scope"""
    x = "global"          
    
    def outer():
        """Enclosing scope"""
        y = "enclosing"   
        
        def inner():
            """Local scope"""
            z = "local"   # Local scope
            
            """Python searches: Local → Enclosing → Global → Built-in"""
            print(z)      # Found in Local
            print(y)      # Not in Local, found in Enclosing
            print(x)      # Not in Local/Enclosing, found in Global
            print(len)    # Not in Local/Enclosing/Global, found in Built-in
        
        inner()
    
    outer()
    
    ----- 
    local
    enclosing
    global
    <built-in function len>
    1. Local – The function checks the local scope of the function to see if the variable was determined there
    2. Enclosing – This is for those hidden variables we talked about with closures. If the interpreter notices there’s a nested function, then it will check one level above to see if the variable was defined there
    3. Global – At first you might think, oh this is the rest of the file and widest scope, which is only half true. The first part is correct, it will check for any globally defined variables (either at the topmost scope of an import or outside of any function definitions). However, there’s actually one last step that is checked
    4. Built-in – This is for the protected keywords, Python’s interpreter will check if the variable you’re trying to call is actually one that comes out of the box (like len()) and will try to call that

    Important: Python stops searching as soon as it finds the variable in any scope. It doesn’t keep looking in outer scopes once it finds a match.

    The nonlocal Keyword

    Example 5. make_avg_err.py: An example closure without a nonlocal variable

    """Before"""
    def make_avg():
        count = 0
        total = 0
    
        def inner(new_val):
            count += 1
            total += new_val
            return total / count
    
        return inner
    
    
    avg = make_avg()
    print(avg(10))
    
    -----
    
    UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

    This is where the nonlocal keyword comes into play. If you try to change a variable from the outer scope (e.g., count = count + 1), Python will freak out and say “I don’t know what count is!” (It thinks you are creating a new local variable).

    Example 6. make_avg_fixed.py: Now using nonlocal to properly scope variables

    """After"""
    def make_avg():
        count = 0
        total = 0
    
        def inner(new_val):
            nonlocal count, total
            count += 1
            total += new_val
            return total / count
    
        return inner
    
    
    avg = make_avg()
    print(avg(10))
    
    -----
    10.0

    Now, this is only to be used when we’re dealing with immutable variables, like int, float, str, etc. Our example above with list does not need a nonlocal keyword because you’re not changing the variable, just the contents of it. list.append(item) simply adds a new item to the existing list, whereas counter += 1 is taking a new int and replacing the value of counter.

    Wrap Up

    Coming from a JS background it’s really easy to try and just do the same things in a diff language without understanding the nuances of the new code you’re writing. I’ve been building out this AST File Parser to go through all of the small things that make Python efficient. I’m hoping to wrap this project up by the end of the month so that I can be prepared for some fun MCP projects. Until then, will be happy to share some other findings in Python like Generators, Decorators, Special Methods, etc.

  • Adapting with the Currents

    Adapting with the Currents

    When I started my Software Engineering journey ~14 years ago, the web development world was in a very different state. WordPress’ PHP and jQuery combination was found everywhere, and the major debate back then when it came to performance was Java Servlets vs PHP. It was a much simpler time to get into the field, but you could start to see where the future would end up going as JavaScript began to implement a lot of the features that jQuery initially made possible

    In 2015, ES6 came out and turned the world of JS development on it’s head. It gave us a lot of the features that we see now (like let and const, as well as Modules) that were unheard of at the time. When this change happened, I promptly decided to begin my move from a WordPress Developer over to becoming a JS-first developer. I joined A100, a web-development bootcamp based in New Haven, CT, and became familiar with the Meteor Framework and was amazed at what it could do and how fast it was compared to PHP at the time. I channeled that energy into my Bachelors, focusing my electives on JS specific options and becoming best friends with my copy of Eloquent Javascript, 2nd Ed.

    After graduating and becoming familiar with JS, I moved to Brooklyn and began working with a boutique branding agency called North Street Creative. While it was still a WordPress shop, we were (and still are) known for our use of animations in design, pushing the browser to it’s limits with native JS + SCSS animations. We built with a motto of “never say no to a designer”, allowing us to build up our front-end skills and become less and less reliant on prebuilt templates and creating our own frameworks. During my time there I saw the leaps and bounds that React, Angular, and Vue were taking and decided to take my shot at picking up those frameworks with my new front-end skills

    After 3 years at North Street, I took a sabbatical to hone my skills and give myself a nice reset between jobs. During this time I built my own LinkedIn clone with a full chat feature based on React and used this during my application process. This demo would get me in front of a recruiter at Capital One, where I currently work as a full-stack Software Engineer. Since joining Capital One I’ve become much more aware of the Architecture part of an Engineer’s path, and continue to level up my skills to this day.

    Every time our field has had a major shift I’ve always been excited to see where it goes. In 2026’s case, the newest generation of AI and the development behind it is the new wild west of Software Engineering, and where I find my next chapter to be. Over the next few months I’ll be delving deep into both AI fundamentals as well as Python (one of the more popular languages used behind the scenes of AI companies), and sharing any updates I have along the way.