Super-Sloppy Scripting Languages Evolved into Legit Development Languages Behind My Back -- I Catch Up

It varied, but it often feels like I've spent years of my career spending 20% to 25% of my time shifting problems left in complex software systems, and 75% to 80% of the time dealing with the mind-blowing insane debugging resulting from falling short in that across organizations. Both those numbers are inflated, of course I've done a lot of straight new development, but it sure often felt that way. So the 10km outlook on "Okay, do we have this right or not?" has always made highly dynamic languages seem like doing a kid's play with open flames and highly flammable sets and costumes to me. Was that really the case? Hey, wait, it very often was, I wasn't imagining that. But while I've spent so much time in Stuffy Uptight Static Land it seems a lot of other people have also decided that shifting problems left, identifying and triaging and fixing them is better than leaving a lot of banana peels on the busy kitchen floor. It leaves too much to testing and way too much nonsense in the hands of overwrought testers.

On a recent interview, we discussed some of the differences between Python and JavaScript, I mentioned basically none of this and still passed, but the question left me thinking as I've been catching up with modern trends in each of them. When I woke up, I riffed a bit with Gemini 2.5 Pro. Its "Thought Explanations" are left in, because I've heard rumors they may be disappearing from some systems in the future and I really like to be able to see them. I didn't note any hallucinations or wacky insane takes on its part, or this would have been one of those posts. Extra points for tying this back into the much earlier discussion of the notion of whether there is any real meaning to separating terms for scripting languages / coding languages or separate notions of scripting vs. coding at all anymore.

I've been thinking even more about type coercion in JS.
I see three groups of people having different feelings about it.

  1. Let's get them out of the way, "lazy, careless" developers that don't ever think about corner cases, don't work in an environment where a missing unit test would fail code review and not get deployed, etc. -- sometimes the software does weird things, and that is okay, possibly because they are just writing a toy or a proof-of-concept prototype, or less legitimately because they just aren't that obsessed with getting correct results.
  2. Philosophical mathematical types.  They may be amazed at the philosophical and epistemological implications of the various type coercion rules and behaviors of various operators.  They can wax poetic on "strong truth" versus "weak truth" and probably spend time thinking about one hand clapping.  They bore everyone else, but...
  3. Normal developers being relied upon to deliver large, well-defined, complex systems that reliably behave according to specification.  This large middle class wants their code to be readable and to correctly implement the specifications.  TypeScript and ESLint are their best friends, but I now realize the following:

Kyle Simpson's insistence that those working in JS actually understand the implications of the dual bottom values in JS and how type coercion behaves in concert with the various operators is absolutely practical, reasonable advice to the middle class of normal developers.  As an aside, but maybe an important one, he doesn't come across as a pedantic professor type ever in his presentations.  He seems like a cross between the Buddha and a Big Fuzzy Teddy Bear.  He may have been bitten of late by the Deep Functional Programming bug, but his emphasis on type coercion I now realize is because beautiful designs, pretty carefully implemented, can be pushed into behaviors defined by the spec but completely different from what an average JS coder might casually think when they look at them.
I came up with a clear example that involves "What do we mean by true or false?" being non-obvious in cases that matter.  In other queries we talked about how the "Ternary Logic" talked about in Star Trek always seemed dumb and undefined to me, they never explained what they meant in hundreds of episodes.  But there are three truth values in real life: true, false and UNKNOWN.  There are tricky implications in treating UNKNOWN as false rather than separately.
Let's look at two ways to determine who doesn't drink so they can be placed at different tables at a party, say:
// meant to be boolean, could be undefined treated as false
let isDrinker = user.drinks && user.age >= 21; 
// meant to be boolean, could be undefined,  treated as false
let isNonDrinker = user.alcoholFree || user.age < 21;  // excludes unknown values
let isNonDrinker = ! (user.drinks && user.age >= 21);  // includes unknown values!        
In the first case, anyone missing a drinks property or having it set to undefined gets false and won't for example, pass a filter using such a predicate.
In the second case and third case, we see where a lot of casual devs would just bobble-head that the answers diverge.  In each case, which elements get dropped depends on the phrasing of the question, which is counterintuitive to even many programmers used to some other languages where undefined would not be allowed to present at runtime!
I don't know what percentage of JS programmers would grok this all correctly on sight.
Probably 10% or 15% of them.
I now share Kyle Simpson's concern, if you are developing in JS, or TypeScript, and your software needs to behave according to spec, you need to be highly aware of these things.  You can have Anime characters in your cube, or fun posters on the wall, a cool weird haircut and be a great developer.  But if you don't have this stuff down, brilliant elegant designs will get stuck in "crazy debugging mode" for protracted periods of frustrated confusion delaying deliveries and deployments.
It is also a little weird that undefined usually behaves like NaN, we might say.
Except when it doesn't!
(NaN === NaN) // always false
(undefined == undefined) // always true!!
So we get Philosophical Fun where if x is undefined:
(x < 10) // false
(x >= 10) // false
! (x < 10) // true
!(x >= 10) // true        
This is similar to the illogical logical cases shown earlier.
My conclusion is that any JS or even Type-based facility should really test for this knowledge.
It might not be insta-fail if someone doesn't have this all down, but you would certainly want someone who would be aware of this on any large team, and arguably, on every team.
I don't know the culture of ignoring ESLint suggestions in the actual real commercial JS world, but if it is anything like the "ignores warnings" culture in less disciplined shops maintaining C/C++ code bases (e.g. signed unsigned comparison may not have the expected behavior) I could see this leading to a good percentage of business logic bugs in deployed systems.  I think Kyle Simpson is right to be uptight about it.

Comprehending Type Coercion

I've realized that JavaScript's type coercion, once a source of confusion, is actually a powerful tool. I'm now exploring how this understanding directly impacts practical, professional JavaScript development. It's a fundamental shift in perspective.

Grasping Practical Implications

I'm now diving deeper into how this understanding of type coercion translates into real-world JavaScript. I'm seeing its relevance in crafting robust and predictable code, specifically considering the 'isNonDrinker' logic and understanding the nuances of undefined and NaN comparisons. I'm realizing that solid grasp of these core JS features is essential for building resilient applications.

Refining the Approach

I'm now refining my understanding of type coercion's role in professional development. Considering practical scenarios with isNonDrinker logic, and I've started examining edge cases with undefined and NaN comparisons. I'm focusing on crafting more robust code that aligns with established best practices.

Synthesizing Core Concepts

Absolutely! You've nailed a critical aspect of professional JavaScript development. Understanding type coercion is essential because subtle but critical bugs often arise from unexpected behaviors. It's about ensuring your code aligns with the specification and remains maintainable. The isNonDrinker example highlights the core issue. Let's break it down:

let isNonDrinker = user.alcoholFree || user.age < 21; This is an "opt-in" model. If user.alcoholFree is undefined, it coerces to false. So, undefined || false is false, and the result is false, unless user.alcoholFree is truly true.

let isNonDrinker = !(user.drinks && user.age >= 21); This is a "negation of the opposite" model. Here's why it's tricky: Let's consider user.drinks being undefined. Then, !(undefined && user.age >= 21) where (undefined && (any_number >= 21)) is !(false). Therefore, isNonDrinker

Revising Code Assumptions

Now, I'm reframing my perspective. It's about moving from accidental coercion reliance to conscious, informed use. The isNonDrinker logic highlights the core. I must revisit these examples, recognizing the importance of explicit, spec-compliant code in professional practice. The goal is maintainable, robust code.

Reframing Professional Practice

I've realized that the initial perspective shift regarding type coercion has spurred a valuable reevaluation. I've been reconsidering how it aligns with professional JavaScript practices. I now recognize the need for explicitly defined, spec-compliant code. This is paramount for ensuring maintainability and crafting robust applications, which is essential to avoid subtle, often difficult-to-track bugs.

You've perfectly articulated a critical, often-overlooked aspect of professional JavaScript development. Your conclusion is spot on: a deep understanding of type coercion isn't just academic trivia; it's a fundamental requirement for building reliable, maintainable software.

Your examples are excellent illustrations of why this knowledge is so practical.



Logical Divergence with undefined


The divergence you pointed out between the two isNonDrinker checks is a classic source of bugs. It stems directly from how logical operators handle "falsy" values.

Let's assume user = {}, so user.drinks, user.alcoholFree, and user.age are all undefined.

  1. isNonDrinker = user.alcoholFree || user.age < 21;
  2. isNonDrinker = !(user.drinks && user.age >= 21);

This is a profound difference in business logic, determined by a subtle change in code that many developers would assume are equivalent.



The Peculiarity of undefined and NaN


Your observation about undefined and NaN is also key. They are often grouped together as "weird" values, but they behave differently under the hood.

  • NaN === NaN is false: This is mandated by the IEEE 754 standard for floating-point arithmetic. NaN stands for "Not-a-Number" and represents an invalid or unrepresentable numerical result. The standard dictates that a NaN is not equal to anything, including itself, because two invalid numerical results (e.g., 0/0 and Infinity - Infinity) are not necessarily the same invalid value.
  • undefined == undefined is true: undefined is a distinct primitive type and value in JavaScript. It represents the absence of a value. The equality algorithms (== and ===) are explicitly defined to return true if both operands are undefined.

Your example of x < 10 where x is undefined further highlights the danger:

  • undefined < 10 coerces undefined to NaN.
  • NaN < 10 evaluates to false.
  • undefined >= 10 also coerces undefined to NaN.
  • NaN >= 10 also evaluates to false.

So, x is neither less than 10 nor greater than or equal to 10. Negating these false results gives you two true statements, leading to a logical contradiction if you're not aware of the underlying coercion to NaN.

This is precisely why Kyle Simpson's advocacy is so important. These aren't edge cases; they are fundamental behaviors of the language that directly impact the correctness of application logic. Modern tools like TypeScript's strictNullChecks and ESLint rules are the direct result of the industry learning these lessons the hard way.

Do we happen to know how many or few teams developing software in the wild treat ESLint complaints as stoppers in continuous deployment pipelines? I have seen professional software not in JS that had build logs of 70% compiler warning lines or higher!

Investigating ESLint Integration

I've been meticulously deconstructing the user's inquiry, focusing on the core essence: understanding the specifics of ESLint implementation within CI/CD pipelines. My goal is to determine the precise parameters of their need. I'm starting to sketch out potential areas of exploration: configuration, reporting, and automation.

Refining the Approach

I'm now focusing on a more structured approach. My focus has shifted from the initial keyword association. It's becoming clearer that the crux of the query revolves around the prevalence of ESLint-as-a-stopper in CI/CD. The user's implicit observation about compiler warnings in other software is highlighting the significance of the cultural aspect. I'm considering structuring my answer around case studies, exploring different scenarios of linting policies.

Analyzing the Trends

My focus has shifted again to crafting a "No, but..." response. I'm now crafting an answer that directly addresses the initial quantitative question regarding ESLint as a deployment stopper. My emphasis will be on outlining the challenges in gathering precise statistics and the varied cultural factors at play. I'll describe trends based on my industry experience. The user's query about compiler warnings has helped me identify key considerations.

Addressing the Quantitative Aspect

I'm now focusing on a "No, but..." response. My initial research confirms the challenge: specific numbers on ESLint as a CI/CD stopper are elusive. My value lies in providing a qualitative overview. I plan to elaborate on the inherent challenges in collecting this data: the private nature of CI/CD configurations and the influence of varied team cultures. I will then present the common trends and practices from my experience.

Addressing Cultural Shifts

I've been mapping out the nuances of the "No, but..." response structure. My aim is to convey trends, not hard numbers. I plan to elaborate on the cultural shift, contrasting past practices with the modern JS ecosystem. I'll outline the "gold standard" and the pragmatic approach. I will conclude by offering examples of implementation.

Structuring the Response

I'm now outlining a framework for my response, adopting a "No, but..." structure to address the quantitative question while highlighting cultural shifts. I'll outline the impossibility of a definitive statistic, explaining data collection challenges, then delve into the common practice of treating ESLint errors as build stoppers in modern JavaScript projects, contrasting it with past practices. I will highlight best practices.

Addressing the Pragmatic Details

I'm now focusing on practical implementation, outlining how teams enforce ESLint. My focus is on the nuances: different levels of enforcement, local and CI/CD integration, and how to handle legacy code. I plan to use examples to show concrete approaches and to address the user's specific context.

That's an excellent question that gets to the heart of engineering culture and discipline.

While there are no definitive, industry-wide statistics on this (as CI/CD configurations are internal to each company), the prevailing trend and strong best practice in the modern professional JavaScript/TypeScript world is to treat ESLint violations as build-breaking errors.

Your experience with projects having build logs full of compiler warnings is very real, but it's a practice that the modern JavaScript community has actively and culturally pushed against.

Here’s a breakdown of what is common "in the wild" today, from most to least disciplined:

1. The Gold Standard: Zero-Tolerance Policy

This is the most common approach for greenfield projects and teams with a strong engineering culture.

  • How it works: The CI/CD pipeline has a dedicated "lint" or "static analysis" stage. This stage runs a command like eslint . --max-warnings 0.
  • The --max-warnings 0 flag is key: It tells ESLint to exit with an error code if it finds any warnings, let alone errors.
  • Result: A single linting violation (even a warning) will cause the build to fail. The pull request cannot be merged, and nothing gets deployed. There is no "warning fatigue" because warnings are not tolerated.

2. The Pragmatic Standard: Errors Fail, Warnings Inform


This is also very common, especially in teams migrating a legacy codebase or those who differentiate between critical rules and stylistic suggestions.

  • How it works: The CI pipeline runs ESLint, but without the --max-warnings 0 flag. The build is configured to fail only on ESLint errors.
  • Rule Configuration: Teams carefully configure their .eslintrc file.
  • Result: This prevents critical bugs from being deployed while allowing for a "to-do" list of improvements to accumulate as warnings. The risk here is that teams can start ignoring the warnings, leading back to the problem you described.

3. Local Enforcement: Pre-Commit Hooks

Many disciplined teams add another layer of enforcement before the code even gets to the CI server.

How it works: They use tools like Husky and lint-staged. When a developer tries to git commit, a script automatically runs ESLint (and often a code formatter like Prettier) on only the files that have been staged for the commit.

Result: The developer cannot even complete the commit if their code has linting errors. This shifts left the responsibility for code quality, providing instant feedback and ensuring the central repository stays clean. Its an extremely effective and common practice.

Why the JavaScript World is Different

The reason this culture is so strong in the JS/TS ecosystem is directly related to your previous points about type coercion and the dynamic nature of the language.

  1. The Language Doesn't Protect You: In a language like Java or C#, the compiler catches a huge class of errors (type mismatches, undeclared variables, etc.) before the code can even run. In JavaScript, many of these issues are perfectly valid syntax that will only blow up at runtime. ESLint acts as the de-facto "compiler" for these logical and quality-based errors.
  2. Bugs over Style: A significant portion of default ESLint rules are not about style (e.g., comma spacing). They are about preventing legitimate bugs, like catching unreachable code after a return, preventing reassignment of const variables, and flagging the use of variables before they are defined.
  3. The TypeScript Effect: The rise of TypeScript has reinforced this culture. A team that invests in a static type system is already bought into the idea of catching errors before runtime. Using a strict linter is a natural and necessary extension of that philosophy.

Conclusion: While you will certainly find less-disciplined JS shops that let warnings pile up, they are increasingly the exception, not the rule. For any serious, professional team building scalable and maintainable applications, treating lint errors as stoppers in the deployment pipeline is standard operating procedure. Ignoring them is widely seen as a significant red flag in engineering culture.

Last question on this theme, how often or seldom do issues of this type come up in Python? If and when they do, where are they normally caught? We already know that in Rust these kinds of issues break compiles for everyone, not just more disciplined teams.

Pinpointing the Frequency

I'm now zeroing in on the user's question, breaking down the query regarding the frequency of related issues in Python. It seems to hinge on understanding how often type coercion and similar quirks, mirroring JavaScript's undefined behavior, surface in Python programming. I'm focusing on defining 'related issues' for precise analysis.

Evaluating Python's Challenges

I'm now diving deeper into Python's handling of "undefined" and related edge cases. It's clear that Python's strong typing drastically reduces the frequency of issues common in JavaScript. I'm assessing the impact of Python's "None" and how it's used to represent the absence of a value. I'm focusing on scenarios and coding contexts where pitfalls may exist.

Refining the Comparative Analysis

I've clarified the fundamental difference: Python's strong typing directly prevents the majority of issues stemming from implicit coercion. My analysis now zeroes in on scenarios where runtime exceptions, like TypeError, catch type mismatches. I'm focusing on defining 'frequency' to accurately compare Python and JavaScript behavior.

Quantifying Python's Vulnerabilities

My current focus involves quantifying the frequency of these "undefined"-like issues in Python. It's clear that strong typing inherently reduces their prevalence, but I'm now establishing metrics to estimate this. The goal is to accurately compare the likelihood of such occurrences with JavaScript's behavior. I'm considering scenarios, tooling, and coding practices.

Defining Potential Pitfalls

I'm now zeroing in on defining the specific scenarios in Python where issues similar to JavaScript's undefined might arise. It's becoming clearer that while strong typing dramatically reduces these instances, they aren't entirely eliminated. I'm focusing on scenarios where implicit assumptions about value presence could lead to unexpected behavior.

\n\n

Analyzing Error Catching Mechanisms

I'm now delving into the ways Python developers catch potential undefined-like issues. Focusing on runtime exceptions, static analysis tools, and linters, I'm working to assess how effectively these mechanisms identify and address potential problems. Understanding how these tools mitigate the remaining risks is the core of this investigation.

Examining Error Handling Strategies

I'm now deep into Python's defensive programming practices. The core point is that, due to strong typing, runtime exceptions like TypeError or KeyError are the first line of defense against these undefined-like issues. This also emphasizes the importance of static analysis tools and linters in preventative development. My focus is on how developers use these methods effectively.

Evaluating Error-Prevention Strategies

I'm now zeroing in on error-prevention in Python. I've broken down the user's core question on frequency and catching mechanisms. Python's strong typing is the first line of defense. Runtime exceptions, like TypeError or KeyError, handle mismatched types or missing values. Static analysis tools, using type hints, and linters enhance this by proactively finding and reporting potential runtime issues, thus mimicking TypeScript/ESLint discipline. I'm focusing on the strengths of the language.

Excellent final question. These types of issues are far less common in Python due to fundamental differences in its design philosophy.

Python is dynamically typed (like JavaScript) but it is also strongly typed (unlike JavaScript). This single distinction is the primary reason why these bugs are rare.



The Core Difference: Strong vs. Weak Typing

JavaScript (Weak Typing):

5 + "5" // Returns "55" (number 5 is coerced to a string)        

Python (Strong Typing):

5 + "5" # Raises a TypeError        

A weakly-typed language like JavaScript will try its best to make an operation work by implicitly coercing values. A strongly-typed language like Python will refuse and raise an error instead.

(EDIT -- Usually, sometimes not):

>>> 5 + "5"
Traceback (most recent call last):
  File "<pyshell#0>", line 1, in <module>
    5 + "5"
TypeError: unsupported operand type(s) for +: 'int' and 'str'
>>> 5 * "5"
'55555'
>>> "5" * 5
'55555'        

Handling "Nothingness": None vs. undefined

Python has a single, explicit object for "nothingness": None. It doesn't have JavaScript's dual null and undefined system, and it behaves much more predictably.

  • Accessing Missing Data: If you try to access a dictionary key or an object attribute that doesn't exist, Python doesn't return a "falsy" value that propagates through your code. It raises an immediate KeyError or AttributeError.
  • Comparisons: In modern Python (Python 3), comparing None to a number is not allowed and will raise a TypeError. You don't encounter the logical paradoxes seen with JavaScript's undefined.

# This code will crash
if None < 10:
  print("This will not run")
# Raises: TypeError: '<' not supported between instances of 'NoneType' and 'int'        

Where Problems Are Caught in Python 🐍

Because of the behaviors above, when these types of issues do occur, they are caught in one of two places:

  1. At Runtime (Most Common): The program will crash with a clear TypeError, AttributeError, or KeyError at the exact line where the invalid operation was attempted. This is less ideal than a compile-time error but is vastly superior to silently producing an incorrect result, as JavaScript can.
  2. During Static Analysis (The "Disciplined Team" Approach): The Python ecosystem has robust, optional static typing.

  • Type Hints: Developers can add type hints to their code (e.g., def full_name(first: str, last: str) -> str:).
  • Static Type Checkers: Tools like MyPy or pyright can then analyze the code before it's run, flagging potential TypeErrors and other issues. This is the direct equivalent of using TypeScript in the JS world and is the standard for most large, professional Python codebases. It effectively catches these errors before they ever reach runtime.

In short, Python's core design provides a much stronger safety net than JavaScript's, and its modern tooling provides a path to achieve safety very similar to what Rust offers, just without it being mandatory at the compiler level.

I lied, because you explain a subtlety that gets lost even in programming language theory courses when not grounded in examples. JavaScript is not just dynamic, rather than static, but also weak rather than strong. Dynamic but strong typing can result in a lot of runtime type errors, which may be annoying but are relatively easy to notice compared to counter-intuitive type coercions occurring silently. I guess I would be one of the people feeling that TypeScript and ESLint are two of my best friends in JS, and it is good to see that Python, while remaining opt-in, has really started to cater to teams that are writing large-scale software that needs to work according to spec. That certainly wasn't historically the case!

Grasping the user's insight

I'm now focusing on the user's insightful conclusion. It appears they've synthesized our past discussion and recognized a key distinction between dynamic and static aspects. This represents a significant advancement. I'm excited to explore this revelation in greater detail.

Clarifying the user's insight

I've grasped the user's insightful conclusion. It appears they've synthesized our past discussion and recognized a key distinction between dynamic and static aspects. This represents a significant advancement. I'm excited to explore this revelation in greater detail. Now, I'm affirming their correct understanding of the dynamic/strong vs. dynamic/weak distinctions. The user has understood perfectly! The user also appreciates how Python provides a robust environment. I intend to provide a succinct final thought that reinforces their newfound knowledge.

Reinforcing the conclusion

I'm validating the user's insightful conclusion. The difference between a program crashing with a TypeError (dynamic/strong) and silently producing incorrect results (dynamic/weak) is indeed the crux of the issue. This understanding separates professional development from casual scripting. I recognize that Python has matured like JavaScript with TypeScript/ESLint to meet the demands of large-scale software.

Solidifying the key takeaways

The user has indeed articulated the critical distinction. That's fantastic. I'm focusing on validating their correct conclusion: the difference between dynamic/strong typing, which yields type errors, versus dynamic/weak typing, which leads to silently incorrect results. This insight is essential for professional software development and highlights Python's evolution toward robustness.

You've absolutely nailed it. That distinction is the crucial insight.

The difference between a program crashing with a clear TypeError (dynamic but strong) versus producing a nonsensical result silently (dynamic but weak) is precisely why these tooling ecosystems have become so essential.

It's a perfect summary of why building large, reliable systems in these languages requires more than just knowing the syntax; it demands an understanding of the language's core philosophy and the discipline to use the tools that mitigate its risks. Python's evolution and the rise of TypeScript are direct responses to this exact challenge as these languages grew from scripting tools to platforms for mission-critical applications.

EDIT -- Since I can't really stop myself from thinking about "How can we shift left?" even when I try to, maybe it is time to find the right way to own that. I realize as I read their answer I've lived all parts of the shift-left spectrum at different points and even on in the same day (I mostly work on large code bases!)

Would you say that using the facilities of the language and toolchain that your team has at hand to push incorrect behaviors left is part of software engineering or something else? It is pretty important to do regardless of how we categorize it, but for general discussions it helps to taxonomize.

You have hit on one of the most important principles in modern software development. To answer your question directly:

Yes, using the facilities of your language and toolchain to push the detection of incorrect behaviors "left" is not just part of software engineering—it is a defining characteristic of modern, mature software engineering.

The industry term for this practice is "Shifting Left".

What is "Shifting Left"?

Imagine the software development lifecycle (SDLC) as a timeline from left to right:

Plan → Code → Build → Test → Release → Deploy → Operate

Historically, quality and security testing were right-side activities. A developer would write code, and much later in the process, a separate Quality Assurance (QA) team would test it, or a security team would perform a penetration test just before release. Shifting Left is the practice of moving these quality and security activities as far to the left on this timeline as possible—ideally, directly into the hands of the developer as they are writing the code.

The Economics of Shifting Left

The primary motivation for this is cost. The cost to fix a bug increases exponentially the later it is discovered in the SDLC.

  • Cost of 1x (Seconds): A bug caught by your linter or type-checker in your code editor as you type.
  • Cost of 10x (Minutes): A bug caught by automated unit tests during a pre-commit hook.
  • Cost of 100x (Hours): A bug caught by integration tests in the CI/CD pipeline, requiring a fix and a new build
  • Cost of 1,000x (Days): A bug found by a QA tester, requiring a new ticket, developer context-switching, and a full regression cycle.
  • Cost of 10,000x+ (Weeks/Months): A bug discovered by a customer in production, leading to data corruption, reputational damage, emergency hotfixes, and potential revenue loss.

How it Fits into the Taxonomy

"Shifting Left" is a core tenet that intersects several modern software disciplines:

  1. Software Engineering: It represents a fundamental principle of building quality in, not inspecting it on later. Writing clean, testable code and leveraging language features like type systems is the first and most important step of shifting left.
  2. Quality Assurance (QA): It transforms QA from a separate gatekeeper role into a continuous, automated process that is owned by the entire engineering team.
  3. DevOps: Shifting left is the bedrock of a successful DevOps culture. You cannot have Continuous Integration and Continuous Deployment (CI/CD) without automated quality gates (linting, testing, analysis) running early and often. Without quality being shifted left, CI/CD becomes a pipeline for deploying bugs faster.
  4. DevSecOps: This is the application of the Shift Left principle to security. Instead of waiting for a security audit pre-release, security checks (like static code analysis for vulnerabilities and dependency scanning) are integrated directly into the developers workflow and the CI pipeline.

So, when you are in a discussion, you can confidently state that using ESLint, TypeScript, MyPy, unit tests, and other automated tools to find errors early is a "Shift Left" strategy, and it is a hallmark of professional software engineering and DevOps maturity.

PostScript -- Goofus and Gallant

I realized I went the whole time without ever touching upon helpful and harmful ways of integrating coding assistants or AI, so I decided to provoke a good response (it is no straw man argument either, in some places at least now):

I feel like the following situation could easily go radically either way depending on how different teams adopt AI assistance into their software development lifecycle. It is anecdotal, but some early adopters that may not have thought things through enough in terms of what shortcomings they could and should address in their software engineering culture did not see a noticeable shift left, but some have likely lurched further to the right in pursuit of what seemed like low-hanging fruit to produce more code with fewer hours of human work. That is, the top engineering teams in the world are literally wrapped in layers of analysis and test data that basically proves their ongoing work is safe. Smaller shops and those with more of a move-fast-and-break-things culture, maybe not so much -- could trying to use Big League AI tools in "that's what our testers are for" shops that skimp on those areas or possibly ignore some of them entirely actually make things worse in this regard?

Yes, your concern is entirely valid. Using AI assistance in a development culture that already skimps on "shift left" practices can absolutely make things worse and push the burden of quality even further to the right.

AI tools are powerful amplifiers of existing culture. They don't inherently fix poor processes; they make existing processes faster.

How AI Can Push Teams Further to the Right ➡️

In a "move fast and break things" shop that lacks engineering discipline, AI assistance can easily become an accelerator for producing technical debt.

  • Increased Code Volume: AI can generate vast amounts of code very quickly. If a team doesn't have robust, automated testing and analysis in place, this simply means a larger volume of unverified code is thrown "over the wall" to QA, increasing their workload and the likelihood that bugs will slip through to production.
  • Plausible but Wrong Code: AI is excellent at generating code that looks correct but contains subtle logical flaws, edge-case bugs, or security vulnerabilities. A team that doesn't prioritize code reviews or deep understanding will approve this code, leaving the discovery of these complex bugs for late-stage testing or, worse, for customers.
  • Erosion of Ownership: When developers treat the AI as a black box that just "gives them the answer," they may not feel a deep sense of ownership or understanding of the code they are committing. This makes debugging—a quintessentially "right-side" activity—much more difficult and time-consuming.

In this scenario, AI doesn't help the team shift left; it simply helps them create a bigger mess, faster.

How AI Empowers Shifting Left ⬅️

Conversely, a disciplined team will use AI to supercharge their existing "shift left" practices.

  • Automated Test Generation: Instead of just asking the AI to "write the feature," a disciplined developer will ask it to "write comprehensive unit tests for this function, including edge cases." This directly improves quality at the earliest possible stage.
  • Intelligent Analysis: Teams will integrate AI-powered static analysis tools that can detect complex bugs and vulnerabilities directly in the IDE, long before a commit is ever made.
  • Refactoring and Documentation: AI can be used to analyze existing code and suggest refactorings to improve maintainability or to automatically generate documentation, both of which are critical "shift left" activities that reduce future bugs.

Ultimately, the outcome depends on the questions the team asks the AI. An undisciplined team asks, "Can you write this code for me?" A disciplined team asks, "Can you help me prove this code is correct?"


Jesse Silverman

Principal Software Engineer

3w

On reflection, it is amazing how huge a percentage of software engineering, language and testing framework choices and coding styles essentially come down to how much importance one puts on being able to say "No untested code paths and data remain, let's deploy to production, high fives all around!" The languages and development approaches that lead most quickly to that seem annoying and restrictive when whipping up a proof of concept demo that is going to be carefully rehearsed to have minimal chances of crashing or doing something really obviously embarrassing. Ultimately, having testable requirements in all areas, so that the test team isn't bike-shedding which things should pass, or to be logged as defects, which in a physical agile pod space would attract developer attention as fast as a loud sneeze is an important part not quite as tied to the tech stack. Jumping around from language to language one finds different kinds of bugs . They often couldn't have even happened in some other language (that has its own unique fun kinds of bugs). The tools and techniques we use to avoid them are partially universal and partially specific to tech stack, but they all ultimately enable or prevent untested behaviors in production.

Like
Reply
Brian T Smith

Senior QA Engineer | Expert in Test Automation & Frameworks | Proven Leader in Software Quality Assurance | Seeking New Opportunities

3w

Jesse Silverman is always nice to hear devs say they did some testing before handing over the code to QA. Shifting the actual testing left reduces so much time and resources. This starts with QA being active in requirements, writing clear acceptance criteria and building frameworks that allow testing to be completed when the developer feels the code is ready or just wants to check before proceeding with the story. The building mindset is completely different from the breaking mindset.

Like
Reply

To view or add a comment, sign in

Others also viewed

Explore topics