How is software engineering changing?

November 3, 2026

Complete Python Testing Guide

Most test suites break every time you refactor, take forever to write, and still miss the bugs that matter. Learn how to write pytest tests that survive change, go fast, and actually catch regressions.

Take Course

Introduction

At Ren Systems, we build a sales intelligence tool for dealmakers. We help them stay on top of things happening in their network, like job changes, acquisitions, product launches, and similar events. To do that, we’re processing millions of articles each day, which includes extracting mentioned people and companies and deciding which of the articles are relevant for our users and why. This gives our users meaningful reasons to reach out to their clients and prospects. Most of the heavy lifting is done by our Python backend that’s running on AWS.

Delivering that required us to use AI way before it became the new norm. From text summarization to named entity recognition to relevance classification for a user. We mostly used open-source models and fine-tuned some of them. The first big change we encountered was the release of ChatGPT in November 2022. We used it to implement the reach-out message composer, which was released only a week later. It made a lot of text-processing tasks much easier. The second big milestone was when Claude Code became good enough that I started using it daily. That was in August 2025. Slowly, everyone started using AI tools more and more. The third major milestone was when every software engineer at Ren Systems went all-in on AI tools, and we stopped typing code altogether. Not everyone was thrilled initially, but since we’re a small team, we got there rather quickly. So how have our engineering practices changed over that time?

Release fast to iterate faster

Ever since I joined Ren Systems in 2021 and took over the backend lead role, I pushed hard for testability and the ability to iterate quickly. My goal was always simple. Building the software that customers love as fast and as reliably as possible. How was this implemented? I encouraged others to practice test-driven development; we started using feature flags; we implemented auto-release to production for every merge to master; I encouraged small, reviewable MRs; green pipelines that mean that we can safely release to production; a simple rollback process in case of issues in production; monitoring and logging that allowed us to spot and fix potential issues early. Once the AI code generation era started, we’ve seen even more clearly how important these things are. We didn’t have to change anything special in our workflows to release up to 50% more MRs per day than before. Most of them stayed roughly the same size as when we were still manually writing the code. We also didn’t notice any increase in the production error rates. This was only a start. To be even more effective, we had to reiterate on many parts of our process to make them better fit for generating code with AI.

Despite early success, we’ve also started seeing new issues. For example, greenfield code was always worse than brownfield code. We kept pointing out the same things in code reviews over and over again. For example, at some point we had three places in CLAUDE.md that stated that inline imports must not be used. Despite that, Claude Code kept using them. We also had to fight the urge to open huge, unreviewable MRs, as it’s so easy to write a lot of code by just prompting the AI with a couple of words. As we’ve always seen AI tools as amplifiers, we’ve started investing in codebase and process improvements.

Standardizing as many things as we can

We noticed quite early that the output is much better when things are standardized inside the codebase. There should be one and only one way to implement a certain type of thing - e.g., a repository. So one of our first projects was to add missing type hints inside tests. Doing that reduced the number of loops Claude Code needed to make tests pass, as typing issues were caught earlier in the process. Another example was enforcement of correct transaction usage for repositories. Some of the early changes implemented by Claude Code backfired due to misuse of database transactions. Methods that should be executed inside open database transactions were called directly. In-memory repositories used inside unit tests didn’t need transactions, and it was easy to miss that during code review. We improved that by abstracting transactions to the level where the same usage was required within in-memory and Postgres repositories. If you forgot to open the transaction with a context manager before calling the method, the error was raised for both. Not having that in place was actually a violation of the Liskov substitution principle. It’s just that we haven’t noticed that before because each of us always remembered to call repository methods within a transaction. But actually, it was exactly the “bird abstraction with a penguin that can’t fly and raises an error when the fly method is called” type of situation. As soon as we added that, Claude Code stopped forgetting to open the transactions, as there was an error reported while running unit tests if the transaction was missing. After that, we started regularly investing in standardizing the codebase. Whenever we notice some discrepancy between two parts of the code, we try to find some common denominator and use it in all relevant places. This way, it’s much more likely that we get the expected output from LLMs the next time that we need to build something similar.

Code style is implemented as a linter

Like many others, we struggled with code reviews for a while. One of the most annoying things was pointing the out the same things out during code reviews. Despite the fact that there were very explicit rules about these things written inside CLAUDE.md. So, at some point, we used Claude to pull all the comments from MRs in GitLab over the last year to see whether we could find some patterns and potentially improve at least some aspects of the AI code generation. For example, no IFs inside tests and no arguments with default values. At that point we asked ourselves: “Can we add some deterministic checks that Claude Code can execute at the end of the work that will give immediate feedback on whether our coding style is respected?” This led us towards implementing our custom linter in Rust. We realized that many of these things can be detected by parsing Python’s AST. The first rule that we’ve added was “No inline imports”. As soon as the rule was added, we stopped seeing inline imports in Claude Code’s final output. Since inline imports weren’t there anymore, we also stopped seeing comments in MRs pointing that out. At this point, we have 14 custom rules implemented on top of enabled ruff’s rules. So, adding a custom linter definitely changed things for the better. The AI-generated code was much closer to our expectations, and we could finally focus on important stuff during code reviews.

AI as part of the code review process

We introduced AI code review early in the AI transformation process. Initially, it was just a Claude Code command that everyone ran on their own machine whenever they wanted to. After that, we added it as an optional job to CI/CD pipelines, and later on we added an MR gate that requires at least one AI code review in the MR. It’s a simple prompt for Claude Code. No special tool or anything, but it does its job well. This didn’t allow us to get rid of human code review. But it made it simpler, as it’s very good at pointing out things like inconsistencies or missed cases in the implementation. On the other hand, it’s awful at evaluating things like “Which things inside an organization will be impacted by this change?” or “Do we really need to build this?” or “Are we going in the right direction?” For example, a new admin panel feature can have consequences way beyond the admin panel itself. It may require some compliance work or a change in certain processes. We try to catch as many of these “not exactly the code” type of issues early in the process, for example, by writing down proposals and reviewing them with the team. However, some things become visible only once the code has already been written. That’s why, as before the AI era, we try to keep MRs small - so that they can be reviewed, updated, and merged rapidly. The usual flow looks like this:

  • Prepare proposal
  • Review proposal with the team
  • Prepare MR with AI
  • Do AI code review
  • Update as per AI code review
  • Ping teammates for code review
  • Update as per comments
  • Release to production

So nowadays, AI code review is yet another part of the feedback loop. Similar to a custom linter, it helps catch potential issues earlier in the process. It also helps code reviewers to focus on aspects that go beyond “this for loop should be a while loop” type of problems.

Using custom harness

The latest improvement is for sure our custom harness. It’s completely custom and built exactly for our workflow. AI gave us the ability to build a native macOS app that brings together all of our systems involved during the software development life cycle. Before that, one had to jump between IDE, GitLab, Sentry, Slack, and Notion. With the custom harness, all things are one single click away. For example, it creates a worktree and a Claude Code session in a sandbox for every new branch so that Claude Code doesn’t have direct access to your machine. To change all the things addressed in unresolved MR comments, you just tell Claude Code to do it. And to open a new MR that links it with the ticket and posts a Slack message, you need to click two buttons. One of my teammates built the first version over a weekend. Before the custom harness, you had to copy and paste links back and forth. Not only did it decrease friction for developers, but it also ensures everything is correctly linked, as there are no manual steps involved. So this made our life much easier. The latest development around this is “slopboxes” - as we call them. This allows us to spin up the whole application that’s deployed in the cloud on our local machine and do all the crazy experiments that we want - end to end. This includes API, Celery workers, web app, and so on. Here we don’t need to worry about breaking something. We can validate the wildest ideas without interfering with the production code in any way. Code from slopboxes is always “throwaway” code. Once we know what we want to build, we just follow the regular workflow. At the moment, we’re still running everything on our local machines, but we might consider spinning things up inside our cloud once it makes more sense for us.

Different model flavours

As usual, it’s not only bells and whistles. One thing we’ve observed over time is that each model has its own flavour. This also impacts the unnecessary or even stupid things that the model keeps doing. For example, Opus models between 4.5 and 4.7 really loved writing very long tests with many IF statements inside the test function body. On the other hand, since Opus 4.8, models are so paranoid that they are adding tests like checking that the removed feature is removed. These things might not seem like much, but they add up over time. Especially with more code being written. Each unnecessary test slows down your process. Each unnecessary complexity in tests decreases their value, which hurts the quality of your feedback loop. So, I think it’s important to always stay on your toes and adapt as new models come out. You might need to extend your linter or update the agents file or something else. One way or the other, that’s certainly the bad part. Maintenance that no one likes now extends to this area as well.

Building everything

AI made building things from just a couple of sentences insanely easy. There’s no doubt about that. Obviously, that incentivises the urge to build everything that we can. There are multiple aspects to that.

First, adding something to the product doesn’t mean that users will actually be able to get any value out of it. It doesn’t make much sense to add a video editor to an online bank, does it? Building everything can quickly push you into a spiral of bad decisions. You quickly gravitate towards too many open MRs, skipped code reviews, and severe bugs in production, which might cost your company serious money. In other words, it doesn’t matter how fast you’re running if you’re going in the completely wrong direction.

Second, there’s also the maintenance part of the story. Longer term, this is many times more time-consuming and token-consuming than the initial building of the solution itself. The time and the amount of tokens available are finite. So you should treat them as such. One shouldn’t build new stuff just because they can.

Nevertheless, this can also be turned into an advantage. The fact that we can build everything can be used to build quick prototypes. Prior to AI, one usually spent a lot of time planning and investigating from different angles before committing to coding something. With AI, coding is cheap. So the whole exploration process can be done in a different way. Especially for the things that have a lot of unknown unknowns. Nowadays we do the following. We take the rough idea and build the prototype. We try it out and write down open questions. Then we build the second prototype from scratch with learnings from the previous iteration, trying to answer the open questions. We repeat the same process until we end up with a proposal for which we’re confident that we can build and that makes sense also from a product perspective. Then we build the thing from scratch for production. For example, I used this approach to build the Salesforce plugin. I built three prototype versions before we went all in and actually built it. Each version gave us new learnings about how Salesforce works, how Ren and Salesforce should interact with each other, where we should store the data, which data we should store in the first place, and so on. Doing that before the AI code generation era would be simply impossible, as it would take too much time. Did this make us 10x faster? For sure not! Yes, we built the plugin much faster than we would without AI, but, more importantly, it helped us to fail and learn fast before any user was involved.

Increased efforts on security

Too many times in the past, security was an afterthought. With the AI explosion, it became a more integral part of the whole software development lifecycle. It’s hard to say whether this is good or bad. But one thing is true. There’s more effort needed to keep things secure, as now any 8th grader with a laptop and a Claude Code subscription poses a security threat. For example, one should keep up with updates to their dependencies, including security patches, while trying to avoid supply-chain attacks. Any long-lived credentials like AWS access keys become a much more serious threat with everybody having AI agents installed on their machines. This requires doing things like switching from username & password database auth to IAM auth, from IAM users to Identity Center users, and so on. One also needs to decide whether to give API access to their AI agents to a certain service. Many times this is not easy, as the APIs don’t have granular (enough) access control. One such example is GitLab. Besides that, phishing and social engineering have become even more involved. So it’s more important than ever to stay sharp and think twice before doing something stupid. Then there’s also a question of which data AI agents should have access to. None of these have a simple answer. One needs to consider trade-offs and decide between not staying behind the industry curve vs. security. All these things might not be visible to every individual contributor, but they for sure eat a lot of time for team leads and CTOs.

Build vs. buy

Last but not least, we’re observing that build vs. buy discussions have changed significantly. In the past, it was usually a no-brainer to simply install an open-source library that you needed. If it was widely used, maintained, and it solved your problem, you didn’t think twice. With the fact that it’s super easy to write the code with AI and with all the supply-chain attacks, one should think twice. When you count in all the recent downtimes and license costs, one really thinks twice. Once again, we end up at maintenance. Nowadays, before installing the library, we ask ourselves: “Do we need the whole library or only a very small part of it? Are there many edge cases to handle to solve the problem? Is the library regularly maintained? What’s happening with issues - are they being solved or just closed?” Then we decide which makes more sense - installing or simply implementing. And it’s similar with SaaS products.

Wrapping things up

One thing we noticed is also that we’re often struggling with wrapping things up. It’s hard to say: “This feature is done as is. Let’s close the epic!” There are two main reasons.

First, it’s too easy to start something new. You’re literally 3 sentences away from starting a new session where AI will build something new. And that’s the easy part. Much easier than finishing the 20% part of the feature that’s 80% done. The last part is always the hardest, and often it involves hard decisions. The more things that are open, the harder it is to close them, as more hard decisions and more context switching are required.

Second, it’s hard to know what’s good enough. That was always the case. Now it’s much easier to “just improve this or that” than before. Consequently, it’s much harder to draw the line and say: “That’s good enough. Let’s close this and move on.” What helps us with this one is following the “When in doubt, simplify!” rule.

At the time of writing, that’s the one with which we’re struggling the most. Mostly because there’s no simple answer to what’s good enough and how many things being developed in parallel are too many. Things are very nuanced from case to case. But we’ll continue improving that. From coders to managers Along this journey, the role of software engineers has changed. At least for the ones who have seen themselves as coders. Personally, I don’t really miss the coding part. But I often hear that people do miss it - including some of my teammates. Before AI code generation, writing the actual code ate up a significant chunk of our workdays. There were also other activities like meetings, architecture reviews, preparing proposals, and reviewing the code to just name a few. And let’s be honest - no one really liked them. Now that AI is writing most of the code, these activities take more time than before. But there’s now one important thing that’s added. Every software engineer is now kind of a software engineering manager. Our responsibility is now also to build workflows, environments, and processes in which AI agents can thrive. This includes improving the feedback loop by adding more rules to custom linters, defining an automated testing strategy, making the code review process more efficient, making CI/CD pipelines more efficient, making the rollback process easier, making monitoring show potential issues earlier, ensuring security and integrity by design, and so on. In my opinion, it became even more crucial to be agile in your thinking and follow one of Ren’s core values, which is “Never enough”. One should dream big and prepare an environment where such dreams, with the help of AI, can become reality.

Become a better engineer, one article at a time.

Practices, mindsets, and habits that actually move the needle. Delivered weekly to your inbox.

Conclusion

As with any other new technology in the past, there are good things, and there are things that are bad or even ugly. For companies like Ren Systems, which are building their own software products, I think the outcomes can be mainly net-positive. That doesn’t mean that they are necessarily only positive. Some problems might be solved or simplified, but then there are new problems arising that we didn’t have before, or they were at least not as pronounced as they are right now. I see AI tools mainly as amplifiers. And it’s usually much easier to amplify bad practices and habits. That’s why I think that the outcomes mainly depend on each team’s culture, practices, and habits. In teams where it took two days to review MRs, AI causes an MR explosion. In our case, with all the above-mentioned improvements, we got to a point where we now merge up to 100% more MRs per day than we did before AI code generation. And we still have plenty of headroom. But more importantly, our development process and our codebase are both in a better state than ever before. Even if all AI services were shut down tomorrow, we would still be able to do more than before the AI wave started. It’s not like we didn’t have repetitive comments on MRs before AI. AI just made it much more visible. Also, it’s not like we didn’t struggle with a lack of standardization before AI. AI just made the immediate effect much more visible. If you want to make things better, stop throwing more AI at it. Take a step back and work on culture first and processes first - then just add the AI as an amplifier.

Share