Wilson Tovar (krthr)

Things I Learned This Week: Patching Pitfalls, Go's OOP Philosophy, and Python Async Adventures

Hey everyone,

I just wanted to share a few recent learnings and musings from the coding trenches. Sometimes, the smallest bugs cause the biggest headaches, and sometimes, a new language makes you question old habits!

Go, OOP, and the Inheritance Conundrum1

All my professional life, I've worked with object-oriented programming (OOP) languages (JavaScript, Java, Ruby, Python, etc.). My brain is wired to think in terms of classes, inheritance, polymorphism, and encapsulation. So, my mindset is pretty aligned with this paradigm.

But now that I'm working on an extensive Golang project, I've encountered this interesting battle in my head between traditional OOP and Go's more declarative, composition-focused approach. The Go FAQ link above is quite direct: "Does Go have OOP? Yes and no." It clarifies that while Go has types and methods and allows an object-oriented style of programming, it doesn't have type inheritance.

Instead, Go strongly encourages composition over inheritance through embedding.

Initially, this felt... limiting. My instinct, when faced with a need to share behavior or create a "specialized" version of a type, was to look for an extends keyword. How do I make a PremiumUser that is a User but with extra features? In Go, you'd typically embed a User struct within your PremiumUser struct:

type User struct {
    ID   int
    Name string
}

func (u *User) HasPermission(p string) bool {
    // Base permission logic
    return false
}

type PremiumUser struct {
    User // Embedding User type
    SubscriptionLevel string
}

// PremiumUser automatically gets User's fields and methods.
// We can also override or add new methods.
func (pu *PremiumUser) HasPermission(p string) bool {
    if p == "premium_feature" {
        return true
    }
    return pu.User.HasPermission(p) // Call embedded type's method
}

The "battle" for me has been retraining my brain. Instead of thinking, "A is a B," I'm learning to think, "A has a B and thus gains its capabilities." It pushes towards smaller, more focused interfaces and can avoid the complexities and tight coupling that deep inheritance hierarchies sometimes create (the "gorilla-banana problem" – you wanted a banana but got a gorilla holding the banana and the entire jungle).

I'm still navigating this shift. There are moments I miss the explicitness of super() or a clear extends relationship. However, I'm also starting to appreciate the simplicity and explicitness that Go's embedding offers. It forces a different kind of design thinking, often leading to more decoupled and flexible systems. It's a journey, and Go is definitely making me re-evaluate some deeply ingrained OOP habits!

Don't Forget to Stop All Patchers (The Pytest Cache Mystery)

I encountered a truly strange issue while running unit tests with pytest2 recently. After running the test command, all the tests were passing, but the execution itself would fail at the very end with an error related to the pytest cache folder:

error: [Errno 2] No such file or directory: 'project/.pytest_cache/README.md'

I was stumped. I couldn't find anything relevant on Google, Claude, nor our internal Amazon wikis. The error message seemed so disconnected from my actual test logic. So, I resorted to the tried-and-true method: reviewing my changes line by line.

And there it was! A patched object that wasn't being stopped after the test's execution. In my setUp method, I diligently start all my patchers, but I'd missed adding one to the tearDown.

The fix was simple, but finding it was the hard part:

class TestUtilHelpers(unittest.IsolatedAsyncioTestCase):
    def setUp(self):
        self.patcher_datetime = patch('your_module.datetime')
        self.mock_datetime = self.patcher_datetime.start()

        self.patcher_mkdir = patch('os.mkdir') # The culprit!
        self.mock_mkdir = self.patcher_mkdir.start()
        # ... other patchers

    def tearDown(self):
        self.patcher_datetime.stop()
        self.patcher_mkdir.stop() # The missing line!

Async Code in Python is Tricky

Python's asyncio4 library, along with the async and await keywords, has been a game-changer for writing high-performance, I/O-bound applications. The ability to handle many concurrent operations without the overhead of threads is incredibly powerful. However, stepping into the world of asynchronous programming in Python isn't always smooth sailing.

A prime example of this challenge, and something I'm actively working through, is building a custom Python logging.Handler. The standard logging framework in Python is synchronous: when your code calls logger.info("message"), it expects that call, including the handler's emit() method, to process the log record and complete relatively quickly. My goal for this custom handler is to send log messages to a cloud service. Naturally, the library for interacting with that cloud service is, and should be, async to avoid blocking I/O.

This immediately presents a conundrum:

How do you bridge this sync/async gap effectively? This specific problem has thrown several general async challenges into sharp relief for me:

Bridging the sync/async divide (the "How do I even run this?" problem):

Thanks for reading,
Wil

  1. https://go.dev/doc/faq#inheritance

  2. pytest documentation

  3. unittest patchers

  4. asyncio documentation

#tech #til