Over the past year, I had the same conversation with almost every team starting an AI initiative. Should we build on Semantic Kernel, AutoGen or Foundry?
At first it felt like the most important architectural decision we’d make. Each framework had its own philosophy, each promised to be the foundation for enterprise AI, and picking the wrong one felt like an expensive mistake. I spent a lot of time helping teams weigh the trade-offs.
Looking back, I think we were asking the wrong question. I certainly was.
I watched teams spend months debating SDKs while the decisions that actually decided whether their applications survived production went unexamined. Some built elaborate orchestration layers for workflows that a few deterministic functions would have handled. Others avoided agent frameworks entirely and later found they’d designed themselves into a corner.
Then Microsoft settled it for us. It introduced the unified Agent Framework, quietly moved Semantic Kernel and AutoGen into maintenance mode, and the debate I’d spent months refereeing was suddenly over. Turns out the answer to “which of the three” was “none of the three, here’s a fourth.” The framework hit version 1.0 and general availability in April 2026, stable across .NET and Python.
What surprised me wasn’t the decision. It was how fast a debate that had eaten so much of our attention stopped mattering. Microsoft changed the menu.
It didn’t change the meal.
The framework was never the hard part
Framework selection dominated almost every early conversation I had about enterprise agents. Which SDK do we standardize on? Which orchestration model gives us the most flexibility? Which one is Microsoft actually betting on?
Fair questions. But after a year of watching these projects play out, I’ve slowly come around to a different view. Those weren’t the questions that decided anything.
The first question I ask now is much smaller. Does this thing actually need an agent?
It sounds obvious, and I still get it wrong sometimes. But it’s the mistake I see most. On one project, a team spent weeks designing a multi-agent workflow for a process that ran the same four steps every time: read a document, validate it, call an API, send a notification. The diagrams looked great. The system in production didn’t. A few well-tested functions would have been easier to build, easier to maintain and a lot easier to trust.
Part of this is just that “agent” has become the word everyone reaches for. Sometimes it’s the right call. Sometimes it’s a workflow we already knew how to build, wearing a newer label. An agent earns its complexity when it genuinely has to decide things you can’t predetermine, choosing between tools, adapting to what it finds, working out its own next step. If you already know every step, you have a workflow, and a workflow is usually the better engineering choice. The consolidation didn’t change that. It just made it easier to see.
What building production agents actually taught me
Once I stopped fixating on frameworks, the same three problems kept showing up. None of them had anything to do with the SDK.
Context beats model choice
Early on I spent a lot of time comparing models, the way you’d agonize over a restaurant menu and then order what you always order. Now I spend most of it thinking about context, which is far less fun and far more useful.
I’ve watched good models fail because they were handed too much, not too little. One team I worked with gave the model access to nearly every internal document they had on the theory that more information meant better answers. It went the other way. Responses got slower, less consistent and sometimes skipped right past the thing that actually mattered. When we cut the context down to only what the task needed, the quality jumped almost immediately. I didn’t predict that. It taught me to be suspicious of “just give it everything.”
The best agent systems I’ve worked on weren’t the ones with the biggest context windows. They were the ones careful about what reached the model, and when. That’s not something the framework hands you.
Failure is where the real work is
Most agent demos look great because they’re built around the happy path. Production doesn’t extend that courtesy.
I remember a project where everything held up in testing. Then a downstream API timed out after the agent had already completed several earlier steps. We couldn’t just restart, because part of the business process had already gone through. We ended up spending far more time on recovery logic than we ever spent on prompts. That project changed how I think about this work. The hard part was never getting the model to make a decision. It was making sure the system didn’t fall apart when reality refused to follow the script.
Tool calls fail partway through. APIs return inconsistent data. Models call the same tool over and over because the last answer wasn’t what they wanted. That’s not the exception; that’s a normal Tuesday. Whether you retry, roll back, pause for a human or push on with partial results is a judgment call, and no framework is going to make it for you.
Identity is the real security boundary
This one surprised me most. The moment an agent stops being a chatbot and starts touching real business systems, identity matters more than orchestration.
Every project gets to the same question eventually. Who is this agent actually acting as? The developer’s credentials? A service account? The user who asked? Get it wrong and you’ve built something autonomous running with more access than any single person should have, which is exactly the kind of thing that looks fine until an audit. The Agent Framework, like most modern tooling, makes it easier to wire agents to tools through standards like the Model Context Protocol. That helps. But where human approval belongs, what needs extra authorization, how much rope to give the thing, those are still yours to decide.
The surprises weren’t technical
Here’s what I didn’t see coming. The hardest part of last year wasn’t technical at all. It was organizational. The moment a team heard “agent,” expectations shifted under everyone’s feet. Business stakeholders started expecting full autonomy. Developers assumed the thing could reason its way through anything. People started designing for flexibility before we’d even agreed on what problem we were solving. The word did damage before any code did. I found myself spending as much time resetting expectations as I did discussing architecture.
Build for change, not for today’s winner
I don’t think the teams that struggled last year picked the wrong framework. Semantic Kernel was reasonable. AutoGen was reasonable. Foundry made sense for plenty of cases. I’d have signed off on any of them.
The ones that got hurt put all their eggs in one framework, treating it as the foundation of the whole system instead of as one more dependency. Microsoft provided a migration path. But teams that had tightly coupled their applications to framework-specific abstractions discovered that migrating and rewriting are not the same thing. That wasn’t Microsoft’s doing. It was their own architecture’s. The teams that moved easily had kept their business logic, prompts and orchestration loose enough to evolve independently of any one SDK. For them, the change was a manageable project, not a teardown.
For what it’s worth, nobody I work with is treating this as an emergency. Most are moving the smaller workloads first, watching how they behave and leaving the production-critical systems alone until they actually understand the new abstractions. That’s the right instinct. And I doubt this is the last consolidation we’ll see, the ecosystem is still young, frameworks will keep absorbing each other and over time the differences between them will be operational more than architectural.
I don’t regret the framework debates, honestly. They were reasonable at the time. What changed wasn’t Microsoft’s roadmap.
It was mine. Watching these systems run in production taught me that the framework is the easiest piece to swap out. Recovery logic, context management, security boundaries, the business workflow itself, those stay with you long after today’s SDK gets replaced by tomorrow’s.
So, Microsoft made one decision easier by turning three frameworks into one. Good. Five years from now we’ll be on different tools, and we’ll still be asking the same handful of questions.
Does this actually need an agent? Does it have the right context? Can it recover when something breaks, because something will? Is it acting as the right person?
Those questions outlast every rewrite. That’s where I’ve learned to put my effort.
Frameworks come and go. Good architecture has to survive all of them.
This article is published as part of the Foundry Expert Contributor Network. Want to join?
EF Core is Microsoft’s flagship ORM (object-relational mapper), the software layer that allows .NET developers to work with relational databases. The DbContext class is the core component of the EF Core framework for managing database operations. However, the DbContext class in EF Core is not thread-safe. Hence, if you share DbContext instances between multiple threads, you will often encounter data corruption issues and the InvalidOperationException.
In this article, we’ll learn how we can execute queries in parallel in EF Core by handling thread-safety issues to avoid concurrency errors. To work with the code examples provided in this article, you should have Visual Studio 2026 installed in your system. You can download Visual Studio 2026 here.
Executing EF Core queries in parallel – the problem
When working in today’s data-driven applications, you will often need to fetch data from multiple unrelated datasets. In applications that use concurrency, thread-safety is critical to guaranteeing correct execution, avoiding data corruption and race conditions, and ensuring data consistency. Let’s understand this with an example.
Let’s say we want to populate a dashboard that displays all recently processed orders, metrics, logs, and traces, as well as your application’s performance metadata. We might write the following code.
public class Dashboard
{
public List Orders { get; set; } = new();
public Metrics Metrics { get; set; } = new();
public List Logs { get; set; } = new();
public List Traces { get; set; } = new();
}
public static async Task LoadDashboardAsync(ProductService productService)
{
Task<List> ordersTask = productService.GetProcessedOrdersAsync();
Task metricsTask = productService.GetMetricsAsync();
Task<List> logsTask = productService.GetRecentLogsAsync();
Task<List> tracesTask = productService.GetTracesAsync();
await Task.WhenAll(ordersTask, metricsTask, logsTask, tracesTask);
return new Dashboard
{
Orders = await ordersTask,
Metrics = await metricsTask,
Logs = await logsTask,
Traces = await tracesTask
};
}
In the preceding code snippet, there are four read operations that are executed by four different Task instances. Our objective is to ensure that the database round trips run in parallel instead of in sequence. We can accomplish this by usingTask.WhenAll, which starts the four tasks, waits for every task to finish, then returns the data wrapped inside a new Dashboard instance.
If we executed these queries sequentially, the user would have to wait until each query completed its execution in turn—for a total wait time equal to the sum of the times for all four queries. However, by running these queries in parallel, we reduce the wait time considerably. The user will need to wait only as long as it takes for the slowest of the four queries to complete its execution.
However, there is a danger with the above approach. If you run multiple operations on the same DbContext instance, you will see an InvalidOperationException with the following message:
A second operation started in this context before the previous operation was completed. This is usually caused by multiple threads using the same DbContext instance; instance members are not guaranteed to be thread-safe.
Databases such as SQL Server, PostgreSQL, and Oracle Database follow a request-response communication model at the connection level: a single connection can process only one command at a time. Hence, you cannot run multiple queries concurrently using the connection. If you await several operations using the same connection, EF Core detects the overlapping use of a non-thread-safe context and throws an InvalidOperationException. To run queries in parallel, you must give each task its own connection or context.
Why DbContext isn’t thread-safe – and how to work around it
The DbContext class in EF Core is designed to manage a single unit of work. To be more precise, EF Core does not provide support for running multiple operations on the same DbContext instance. This design approach creates inherent challenges when you use the same DbContext instance across multiple threads. If DbContext were thread-safe, extensive locking would be required, which would degrade data access performance.
This stateful design of DbContext makes it unsuitable for concurrent access patterns that involve loading, modifying, or tracking different sets of data simultaneously, because it needs to maintain the internal representation of database state.
The change tracker is one of the most important components of DbContext in EF Core. It monitors all entities loaded into memory and detects any changes made to them after they have been loaded. It keeps track of the original, current, and changed values of the entities, thereby enabling the EF Core runtime to know the current state of these entities when you call the SaveChanges() method on the DbContext instance.
To implement thread-safety when working with DbContext, we must write our code to ensure that each concurrent operation gets its own copy of a short-lived instance. Now, we could accomplish this by wrapping a shared DbContext instance inside a thread-safe block using the lock keyword, so that all calls to the database take place using one and only one thread at a time. This approach is illustrated in the code snippet below.
using Microsoft.EntityFrameworkCore;
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Quantity { get; set; }
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options) : base(options) { }
public DbSet Products => Set();
}
However, while the above approach gives us the thread-safety we need, it can degrade data access performance considerably. A better approach is to use IDbContextFactory , which creates fresh DbContext instances on demand. Calling its CreateDbContext() method is cheap and produces a fresh, isolated context every time.
The following code snippet shows how you can register an instance of type IDbContextFactory as a singleton. You can safely call this code from any thread.
Executing EF Core queries in parallel – the solution
Now let’s see how we can put IDbContextFactory to work. The following code illustrates a class named ProductService that uses a factory to create DbContext instances for each scope of work.
public class ProductService
{
private readonly IDbContextFactory _factory;
public ProductService(IDbContextFactory factory)
=> _factory = factory;
public async Task GetByIdAsync(int id)
{
await using var context = await _factory.CreateDbContextAsync();
return await context.Products.FindAsync(id);
}
public async Task UpdateStockQuantityAsync(int id, int updateQuantity)
{
await using var context = await _factory.CreateDbContextAsync();
var product = await context.Products.FindAsync(id);
if (product is null) return;
product.Quantity += updateQuantity;
await context.SaveChangesAsync();
}
}
Note that ProductService has two methods, GetByIdAsync and UpdateStockQuantityAsync. An instance of the DbContext class is created locally in each of these methods. Now, suppose you have two threads, T1 and T2, that execute these methods concurrently. That is, thread T1 executes the GetByIdAsync method while thread T2 executes the UpdateStockQuantityAsync method. Because each of these methods is executed in isolation, they will have their own context, connection, and change-tracking information, and there will be no mutable state, so you don’t need to implement thread synchronization in either of these methods.
Consider the following code that executes a read operation and an update operation in two separate tasks.
The Task.WhenAll method runs the two tasks in parallel and waits until both have finished. The reason this approach is thread-safe, and will not create concurrency errors, is that each of these two methods creates its own DbContext instance internally. Therefore the read operation and the update operation use independent DbContext instances.
Use DbContext pooling to reduce allocation cost
Although creating DbContext instances is not that costly, you should consider using pooled contexts in applications that require high scalability and high performance. The following code snippet shows how you can register a pooled context.
A call to AddDbContext() will register a DbContext instance as scoped per HTTP request. Each request will run on a different thread and each will have its own context. However, keep in mind that the default scoped registration of the DbContext will not always suffice.
You will need a factory to create instances of DbContext when you’re using a background service, or performing some work inside a particular request, or running some business logic operation over multiple contexts.
Key takeaways
If you use EF Core in the data access layer of your application, you must implement thread safety measures whenever you run your queries in parallel.
You cannot execute multiple queries in parallel in EF Core using the same DbContext instance.
The IDbContextFactory enables you to create a DbContext instance for each thread, thereby enabling you to work with these instances in isolation.
Although using a DbContext pool involves a small allocation overhead, it becomes a non-issue if you need high throughput.
A new version of Python’s native package management tool, pip, will remove a limitation that has frustrated Python developers for years. If you wanted to install the dependencies for a given package, but not install the package itself, you were stuck. Either you had to extract the dependency list from the package and install it by hand, or you had to build the whole package anyway.
Why was this a problem? Sometimes, you want only the dependencies for a package—for instance, as a way to create a separate environment for testing or another project. If you’re making source distributions via CI, some requirements might be needed to make the source distribution but aren’t actually included in it (e.g., Cython). You would need to install these requirements somewhere—apart from the project itself—to perform the build step.
A dependencies-only install mode for packages is a long-requested feature. Developer James O’Claire found many examples of such requests, along with various workarounds. Most of those involved third-party solutions of some sort.
Now, a new feature set to land in pip version 26.2 will fix this problem from the inside. The command pip install --only-deps will install only the dependencies for a given package. Note that it will not install build dependencies for the package, but only runtime dependencies.
Note that you can accomplish this with existing third-party tools. For instance, uv sync --no-install-project has the same behavior. But having this functionality right inside pip means you don’t have to turn to external tooling—or ugly hacks—to solve the problem. This may be an example of how third-party projects like uv (a superfast Python package installer) are inspiring native Python features where they make sense.
pip 26.2 is scheduled to be released by the end of July 2026.
G# (GSharp)is moving forward as a programming language for Microsoft’s .NET platform, touted as bringing Go-, Kotlin-, and Swift-style ergonomics to the CLR (Common Language Runtime). The language is described by its creators as modern, simple, and accessible.
Although pre-1.0 and still growing, G# aims to be for people who want a small, predictable language with direct access to the .NET ecosystem. Developers will see imports, func, structs, slices, maps, channels, go, select, and for in iteration. Also important are nullable flow, direct calls into the CLR (Common Language Runtime), and built-in concurrency.
With G#, copyrighted in 2026, developers get value-oriented structs, reference-oriented classes, data structs, and data classes. For concurrency, G# uses scope for structured concurrency, async funcand awaitfor task-based asynchrony, and async sequence[T] for asynchronous streams. G# also makes use of the same Task and Task[T] types familiar from the .NET BCL (Base Class Library).
G# documentation is available on the GitHub site of Microsoft software engineer David Obando. “Every .NET type—your packages, third-party NuGet packages, the BCL—is callable from G# with the syntax you already know. CLR generics use G#’s bracket spelling, and method calls, properties, indexers, and for inover IEnumerable[T] all just work,” according to the website.
A Visual Studio Code extension for G3 can be found at marketplace.visualstudio.com. The extension adds syntax highlighting, language server features, build/run commands, and debugger configuration for .gs and .gsproj files. Developers can install the extension from within VS Code (search for “G#” in the Extensions view) or from the command line.
The evolution of Java is the subject of a just-released documentary about the programming language and development platform. “The Java Story: The Official Documentary” tells the story of Java through interviews with the engineers who created it and shepherded it through three decades.
Produced by CultRepo and sponsored by Oracle, JetBrains, IBM, and Azul, the documentary follows Java from its set-top box and browser-based origins at Sun Microsystems in the 1990s and through its rise to dominate server-side computing in the 2000s, the “dark ages” and resurgence with Java 8 under Oracle in the 2010s, and its continuing modernization and promising role in AI today. “From its humble beginnings as a project code-named ‘Oak’ at Sun Microsystems to becoming a global standard for enterprise software and billions of devices, Java’s journey is one of radical innovation, strategic pivots, and enduring community strength,” said Cult.Repo.
The documentary also delves into Sun’s bitter Java licensing dispute with Microsoft, Oracle’s suit of Google over its use of Java APIs Android (Google won), the creation of the Java Community Process, Sun’s open-sourcing of Java, and Oracle’s switch to the six-month release cycle. Technical enhancements such as lambda expressions in Java 8, virtual threads in Java 21 (Project Loom), and the ongoing refactor to bring value objects to the Java object model (Project Valhalla) also get attention.
Technical experts and other Java figures interviewed in the documentary include James Gosling, creator of Java; Kim Polese, Java’s first product manager; Carla Schroer, director of Java compatibility at Sun Microsystems; James Duncan Davidson, creator of Apache Tomcat; Mark Reinhold, chief architect of the Java Platform Group at Oracle; Brian Goetz, Java language architect in the Java Platform Group at Oracle; Rod Johnson, creator of Spring; and Gavin King, creator of Hibernate.
Preview 6 of Microsoft’s .NET 11 software development platform is now available, with improvements across the runtime, libraries, frameworks, and C# and F# programming languages.
Announced July 14, .NET 11 Preview 6 can be downloaded from dotnet.microsoft.com. The full, general production release is expected in November. The first preview was unveiled February 10.
Runtime-async, the .NET runtime’s built-in implementation of async/await, keeps getting faster in Preview 6, building on the suspension improvements from Preview 5. Async continuations can now opt out of ExecutionContext capture and restore. And the JIT now compiles a dedicated runtime-async version of a synchronous, task-returning method rather than delegating to it through a thunk. The JIT turns the method’s tail calls into runtime-async calls and awaits the task that would otherwise have been returned, so the async path no longer pays for an extra layer of indirection, Microsoft said.
In library improvements, System.Text.Json can now serialize and deserialize the new C# union types. The serializer recognizes a union through the new JsonTypeInfoKind.Union contract type, reads and writes the active case, and supports the reflection-based serializer and the source generator. The new JsonUnionAttribute, JsonUnionCaseInfo, and type-classifier APIs enable customization of how cases are discovered and named.
In C# improvements, extension members now include indexers, so developers can add this[...] access to a type from an extension block. This rounds out the extension members feature introduced in .NET 10, which already supported extension methods and properties. An extension indexer is declared like an instance indexer inside an extension block.
Other highlights in .NET 11 Preview 6:
Math.BigMul(long, long, out long) is now significantly faster on x64. The JIT generates a single MUL r/m64 instruction when both operands are 64-bit values and the caller requests the high half of the result. Previously the JIT emitted a helper call. The change eliminates the call overhead and makes code that multiplies large numbers tighter.
Minimal API validation in ASP.NET Core now supports asynchronous validators end-to-end.
For F#, the compiler emits better sequence points for several common debugging scenarios. Breakpoints and stepping are improved for call arguments that need stack-empty debug points, for expressions and comprehensions, simple literal bindings, and if or match conditions.
For .NET MAUI, .NET 11 Preview 6 advances the cross-platform CollectionView and Shell architecture, removes the legacy Xamarin.Forms compatibility package, and brings the Apple workloads onto stable Xcode 26.6.
Also for .NET MAUI, HybridWebView JavaScript interop is reworked to use a source generator instead of reflection, so it no longer produces trim or AOT (ahead of time) compiler warnings and works correctly in fully trimmed and Native AOT apps.
Projects that use Microsoft.TypeScript.MSBuild in Razor Class Libraries now properly integrate TypeScript compilation outputs with ASP.NET Core Static Web Assets.
With this new codebase, TypeScript 7.0 brings native code speed, shared memory multithreading, and a number of new optimizations that typically yield speedups between 8x and 12x on full builds, Microsoft said in a July 8 announcement. This port of TypeScript to native code was done as faithfully as possible, writing new code while maintaining the structure and logic of the original codebase to keep results consistent and compatible between the two compilers, according to Microsoft.
TypeScript 7.0 is available via npm:
npm install -D typescript
To help with the TypeScript 6.0 to TypeScript 7.0 transition process, Microsoft has published a new compatibility package, @typescript/typescript6. This package provides an executable named tsc6, so that if needed, a developer can install TypeScript 7.0 (which ships its own tsc binary) side-by-side with TypeScript 6.0 without naming conflicts. The new package also re-exports the TypeScript 6.0 API, so that a developer can use tsc for TypeScript 7, while other tooling can continue to rely on TypeScript 6.0.
TypeScript 7.0 is a major milestone in the TypeScript project, Microsoft said. This port has been the primary focus for Microsoft’s team for more than a year. With TypeScript 7.0 now out, the team will return to new feature work, ergonomic improvements, more performance wins, and implementing a new API for the broader ecosystem. Microsoft expects a fairly similar timeline to releases prior to TypeScript 7.0, with new versions published every three to four months.
Meta’s long-awaited Pyrefly linter is out in a 1.0 version, and the forthcoming Python 3.15 has a super-efficient sampling profiler. Plus we have a comprehensive rundown of Python’s indispensable virtual environments — and a warning about a novel breed of malware that exploits Python’s package ecosystem.
Top picks for Python readers on InfoWorld
How to use virtual environments in Python Isolate and protect your Python projects from each other, and empower them to do more, with virtual environments and their native-to-Python tooling.
Pyrefly 1.0: A fast, forward-looking Python linter The first full release of Meta’s long-awaited linting and type checking tool for Python delivers speed and offers advanced features for type-checking PyTorch and Django projects.
Hands-on with the new sampling profiler in Python 3.15 Among Python 3.15’s best new features is a sampling profiler, for instrumenting your code and finding its bottlenecks with a minimum of performance impact or fuss. See up-close how it works.
Python Steering Council calls for temporary pause on JIT project The requested pause stays in place until a proper Standards Track PEP lands for the experimental JIT (just-in-time) compiler, the better to describe how the JIT will be a formal and supported part of Python.
Pyodide 314.0: Pyodide packages on PyPI Thanks to PEP 783, Python packages built with Pyodide (Python ported to WebAssembly) can be installed straight from PyPI instead of through Pyodide — another step closer to Py-on-Wasm becoming an everyday thing.
All about that Python 3.14 garbage collection rigmarole A new garbage collector introduced in Python 3.14 was yanked at the last minute due to reports of higher memory usage. Here’s a deep dive into what changed for the worse and why.
Are you really expected to run five type checkers now? No, but you should keep your options open. This blog post from a Pyrefly contributor recommends choosing one of the major offerings (Mypy, Pyrefly, Pyright, ty, Zuban, etc.), but also getting to know the others too.
Understanding a reactivity model in the abstract is useful, but it is ultimately incomplete without seeing how it shapes real application code. Concepts such as state, derivation, and explicit dependencies only become meaningful when they influence how forms are built, validated, and maintained in practice.
This article focuses on a concrete example: a modest but realistic registration form. Rather than introducing new concepts, the goal here is to make earlier ideas tangible. We will see how a signal-backed model reshapes validation, interaction state, and submission logic, and how much coordination logic simply disappears when form behavior is expressed declaratively.
The focus here is not on novelty or completeness, but on making the underlying ideas easier to reason about. By walking through a signal-first form from model definition to submission, we can evaluate whether this approach truly reduces complexity and where it introduces new trade-offs that teams should understand before adopting it more broadly.
With the conceptual groundwork in place, we can now turn theory into a concrete implementation. In this section, we will build a fully working registration form using Angular’s Signal Forms API. This example is deliberately modest in scope, but it is designed to serve as the foundation for the rest of the series. Each subsequent article will extend this same example rather than introducing a new one.
The form collects an email address, a password, a confirmation password, and explicit acceptance of terms. While simple on the surface, this structure allows us to explore field-level validation, cross-field constraints, interaction state, and submission behavior, all without reverting to event-driven form logic.
Project setup and structure
The example assumes a standard Angular application created with the Angular CLI and configured to use Signals (Angular 17+). The Signal Forms APIs (Angular 21+) live under @angular/forms/signals, which must be explicitly imported.
Separating the model from the component keeps form state independent of presentation. This becomes increasingly valuable as the form grows or is reused across multiple components.
Defining the form model
We begin by defining the shape of the data that the form collects. This is a plain TypeScript interface with no Angular dependencies. Treating the form model as a simple data structure reinforces the idea that the form’s values are just state.
This interface mirrors what would typically be sent to a back-end API. There is no duplication of state, no separate “form value” object, and no mapping required at submission time.
Creating the signal-backed form
The form itself is created in the component using a writable signal as the source of truth. The form() function attaches form semantics validation, field state, and submission to that signal.
// registration.component.ts
import { CommonModule } from "@angular/common";
import { Component, signal } from "@angular/core";
import {
email,
form,
FormField,
required,
submit,
} from "@angular/forms/signals";
import { RegistrationData } from "./registration.model";
@Component({
selector: "app-registration",
imports: [FormField, CommonModule],
templateUrl: "./registration.html",
styleUrl: "./registration.css",
})
export class Registration {
readonly model = signal({
email: "",
password: "",
confirmPassword: "",
acceptedTerms: false,
});
readonly registrationForm = form(this.model, (schema) => {
required(schema.email, { message: "Email is required" });
email(schema.email, { message: "Enter a valid email address" });
required(schema.password, { message: "Password is required" });
required(schema.confirmPassword, {
message: "Please confirm your password",
});
required(schema.acceptedTerms, {
message: "You must accept the terms to continue",
});
});
async onSubmit(event?: Event) {
event?.preventDefault();
await submit(this.registrationForm, (value) => {
console.log(value());
// Mock Server Call
return Promise.resolve([
{
kind: "EmailAlreadyExists",
field: this.registrationForm.email,
error: { kind: "server", message: "Email already taken" },
},
]);
});
}
}
Several design decisions are worth noting.
First, the model signal is defined as read-only. All mutations to the model occur through form bindings, not ad hoc assignments in the component. This keeps the component declarative and avoids the temptation to manipulate form state imperatively.
Second, validation is declared in one place. The schema function describes constraints on the model without introducing control trees, validator arrays, or observable pipelines. Angular takes responsibility for re-running validation whenever the model changes.
Finally, submission logic is explicit. The submit() helper ensures that the form is valid before invoking the callback, and it passes the current model value directly. There is no need to check flags or manually extract values.
Binding the form to the template
With the form defined, the next step is to bind it to the template. Signal Forms provide the [formField] directive, which connects an input element directly to a field in the form schema.
@if (registrationForm().errors().length > 0) {
@for (error of registrationForm().errors(); track error.message) {
What stands out here is the absence of indirection. Each input binds directly to a field. Validation state is accessed through signals such as invalid() and touched(). Error messages are read from a structured error object, not reconstructed manually.
This template contains no subscriptions, no async pipes, and no event handlers for value changes. The UI simply reflects the current form state.
Interaction state and user experience
One of the common criticisms of declarative form models is that they obscure user interaction logic. Signal Forms address this directly by exposing interaction metadata as signals.
The touched() signal determines whether a field has been interacted with. By combining it with invalid(), we control when validation messages appear. This logic remains purely declarative: the template describes when errors should be visible, and Angular ensures the signals stay up-to-date.
The disabled state of the submit button is derived from registrationForm.invalid(). There is no need to manually enable or disable it in response to events. If the form becomes valid, the button is enabled automatically.
Why this scales
Even at this early stage, several advantages of a signal-first form model are apparent. The form’s behavior is expressed in terms of state and derivation, not events. The model, validation rules, and UI bindings are clearly separated. There is no duplication of logic between the component and the template.
As the form grows, this structure holds. Additional fields introduce additional schema entries and template bindings, not new subscription logic. Cross-field validation can be added declaratively. Asynchronous validation and persistence can be layered on without rewriting the core model.
Most importantly, the form remains inspectable. At any point during execution, the model signal reflects the current state of the form. Derived state validity, errors, and UI flags can be understood by reading the code, not by tracing runtime behavior.
What we did not solve yet (and why)
At this stage, it would be easy to walk away with the impression that Signal Forms eliminates most of the hard problems associated with form handling. That impression would be misleading. What we have built so far is intentionally incomplete, not because the approach falls short, but because introducing too much too early obscures the value of the underlying model.
One area we have deliberately postponed is cross-field validation that expresses richer business rules. Many real-world forms depend on relationships between fields rather than isolated constraints. Password confirmation is a familiar example, but more complex scenarios quickly arise in enterprise applications. While Signal Forms support these patterns, introducing them before establishing a clear understanding of derived state risks turns validation back into an imperative exercise rather than a declarative one.
We have also avoided asynchronous validation. Server-backed checks introduce latency, partial failure, cancellation, and race conditions. These are not trivial concerns, and treating them casually often leads to subtle bugs and confusing user experiences. Although Signal Forms provide the necessary hooks to model asynchronous behavior, doing so responsibly requires a careful discussion of pending state, effects, and life-cycle boundaries. That discussion belongs in its own article.
Another omission is persistence and synchronization. Many forms need to autosave drafts, synchronize state with local storage, or react to changes by triggering external side effects. These behaviors are not part of the form state itself; they are consequences of state changes. Treating them as such is essential to keeping the architecture comprehensible. Introducing persistence too early would blur the distinction between state and reaction that this article has worked to establish.
Finally, this article has not addressed migration and interoperability. Few teams are starting from a blank slate. Most will adopt Signal Forms incrementally within applications that already rely on reactive forms or template-driven forms. Hybrid approaches, bridging strategies, and gradual refactors are all critical topics, but they presuppose familiarity with both paradigms. Addressing migration before establishing a solid signal-first mental model would undermine that foundation.
These omissions are intentional. A form architecture that tries to do everything at once often ends up doing nothing clearly. By focusing on the core ideas of state, derivation, and declarative validation, we create a base that can absorb additional complexity without collapsing under it.
Signal Forms in the context of Angular’s evolution
To fully appreciate Signal Forms, it helps to step back and view them not as an isolated feature, but as part of a broader shift in Angular’s design philosophy.
For much of its history, Angular emphasized declarative templates paired with imperative coordination in component classes. RxJS became the backbone of that coordination, providing a powerful abstraction for handling asynchronous workflows, user input, and external events. This model scaled well, but it also encouraged developers to express state indirectly through streams and subscriptions.
Signals represent a deliberate recalibration. They re-center Angular’s reactivity model around state and derivation, rather than events and emissions. This shift is visible across the framework: in component inputs, change detection, and now forms. Signal Forms are not an attempt to replace everything that came before; they are an attempt to make the most common use case, modeling and deriving state, simpler and more explicit.
Framed this way, the design of Signal Forms aligns more closely with state-driven form behavior. The requirement to start with a model signal reflects the idea that the state should have a single, inspectable source of truth. Schema-based validation aligns with the notion that constraints are properties of state, not behaviors triggered by events. Field state exposed as signals reinforces the idea that validity, errors, and interaction metadata are derived values that should be read, not managed.
It is also worth noting that Signal Forms do not attempt to abstract away form behavior. They do not hide form state behind opaque classes or life-cycle hooks. They do not require developers to think in terms of control hierarchies or subscription graphs. Instead, they expose form behavior directly, making it easier to reason about how values, validation, and UI feedback relate to one another.
This approach aligns closely with other recent changes in Angular, including the introduction of modern template control flow and a stronger emphasis on explicit data dependencies. Together, these features point toward a framework that favors clarity over indirection and composition over orchestration.
Importantly, Signal Forms are still evolving. Their APIs may change, and their surface area will almost certainly expand. That is precisely why grounding them in first principles matters. Developers who understand why Signal Forms work the way they do will be far better equipped to adapt as the APIs mature.
This article has intentionally avoided duplicating documentation or enumerating every available feature. Instead, it has focused on establishing a conceptual framework that makes the official APIs feel intuitive rather than surprising. When viewed this way, Signal Forms are not a new way to write forms; they are a clearer expression of what forms have always been.
A new way to think about forms
Building the registration form in this article reveals a quiet but important shift. The reduction in complexity does not come from fewer features or simpler requirements. It comes from expressing form behavior in terms of state and derivation rather than orchestration and reaction.
By treating the data model as the single source of truth, validation rules as declarative constraints, and UI behavior as derived from current conditions, much of the coordination logic that typically surrounds forms becomes unnecessary. There are fewer subscriptions to manage, fewer flags to synchronize, and fewer life-cycle concerns to reason about. Form behavior becomes easier to inspect because it is visible directly in the relationships between values.
This approach does not eliminate the hard problems associated with forms. Asynchronous validation, persistence, and interoperability with existing Angular Forms APIs still require careful design. What changes is where that complexity lives. Instead of being interwoven with state representation, those concerns are layered explicitly on top of a clear foundation.
Signal-first forms are not a universal replacement for existing patterns, nor are they a shortcut to simpler applications. They are, however, a strong example of how aligning APIs with first principles can reduce cognitive overhead and improve maintainability over time. For teams building large, state-heavy forms, this alignment can make the difference between code that merely works and code that continues to evolve without friction.
Understanding a reactivity model in the abstract is useful, but it is ultimately incomplete without seeing how it shapes real application code. Concepts such as state, derivation, and explicit dependencies only become meaningful when they influence how forms are built, validated, and maintained in practice.
This article focuses on a concrete example: a modest but realistic registration form. Rather than introducing new concepts, the goal here is to make earlier ideas tangible. We will see how a signal-backed model reshapes validation, interaction state, and submission logic, and how much coordination logic simply disappears when form behavior is expressed declaratively.
The focus here is not on novelty or completeness, but on making the underlying ideas easier to reason about. By walking through a signal-first form from model definition to submission, we can evaluate whether this approach truly reduces complexity and where it introduces new trade-offs that teams should understand before adopting it more broadly.
Implementing a signal-first registration form
With the conceptual groundwork in place, we can now turn theory into a concrete implementation. In this section, we will build a fully working registration form using Angular’s Signal Forms API. This example is deliberately modest in scope, but it is designed to serve as the foundation for the rest of the series. Each subsequent article will extend this same example rather than introducing a new one.
The form collects an email address, a password, a confirmation password, and explicit acceptance of terms. While simple on the surface, this structure allows us to explore field-level validation, cross-field constraints, interaction state, and submission behavior, all without reverting to event-driven form logic.
Project setup and structure
The example assumes a standard Angular application created with the Angular CLI and configured to use Signals (Angular 17+). The Signal Forms APIs (Angular 21+) live under @angular/forms/signals, which must be explicitly imported.
Separating the model from the component keeps form state independent of presentation. This becomes increasingly valuable as the form grows or is reused across multiple components.
Defining the form model
We begin by defining the shape of the data that the form collects. This is a plain TypeScript interface with no Angular dependencies. Treating the form model as a simple data structure reinforces the idea that the form’s values are just state.
This interface mirrors what would typically be sent to a back-end API. There is no duplication of state, no separate “form value” object, and no mapping required at submission time.
Creating the signal-backed form
The form itself is created in the component using a writable signal as the source of truth. The form() function attaches form semantics validation, field state, and submission to that signal.
// registration.component.ts
import { CommonModule } from "@angular/common";
import { Component, signal } from "@angular/core";
import {
email,
form,
FormField,
required,
submit,
} from "@angular/forms/signals";
import { RegistrationData } from "./registration.model";
@Component({
selector: "app-registration",
imports: [FormField, CommonModule],
templateUrl: "./registration.html",
styleUrl: "./registration.css",
})
export class Registration {
readonly model = signal({
email: "",
password: "",
confirmPassword: "",
acceptedTerms: false,
});
readonly registrationForm = form(this.model, (schema) => {
required(schema.email, { message: "Email is required" });
email(schema.email, { message: "Enter a valid email address" });
required(schema.password, { message: "Password is required" });
required(schema.confirmPassword, {
message: "Please confirm your password",
});
required(schema.acceptedTerms, {
message: "You must accept the terms to continue",
});
});
async onSubmit(event?: Event) {
event?.preventDefault();
await submit(this.registrationForm, (value) => {
console.log(value());
// Mock Server Call
return Promise.resolve([
{
kind: "EmailAlreadyExists",
field: this.registrationForm.email,
error: { kind: "server", message: "Email already taken" },
},
]);
});
}
}
Several design decisions are worth noting.
First, the model signal is defined as read-only. All mutations to the model occur through form bindings, not ad hoc assignments in the component. This keeps the component declarative and avoids the temptation to manipulate form state imperatively.
Second, validation is declared in one place. The schema function describes constraints on the model without introducing control trees, validator arrays, or observable pipelines. Angular takes responsibility for re-running validation whenever the model changes.
Finally, submission logic is explicit. The submit() helper ensures that the form is valid before invoking the callback, and it passes the current model value directly. There is no need to check flags or manually extract values.
Binding the form to the template
With the form defined, the next step is to bind it to the template. Signal Forms provide the [formField] directive, which connects an input element directly to a field in the form schema.
@if (registrationForm().errors().length > 0) {
@for (error of registrationForm().errors(); track error.message) {
What stands out here is the absence of indirection. Each input binds directly to a field. Validation state is accessed through signals such as invalid() and touched(). Error messages are read from a structured error object, not reconstructed manually.
This template contains no subscriptions, no async pipes, and no event handlers for value changes. The UI simply reflects the current form state.
Interaction state and user experience
One of the common criticisms of declarative form models is that they obscure user interaction logic. Signal Forms address this directly by exposing interaction metadata as signals.
The touched() signal determines whether a field has been interacted with. By combining it with invalid(), we control when validation messages appear. This logic remains purely declarative: the template describes when errors should be visible, and Angular ensures the signals stay up-to-date.
The disabled state of the submit button is derived from registrationForm.invalid(). There is no need to manually enable or disable it in response to events. If the form becomes valid, the button is enabled automatically.
Why this scales
Even at this early stage, several advantages of a signal-first form model are apparent. The form’s behavior is expressed in terms of state and derivation, not events. The model, validation rules, and UI bindings are clearly separated. There is no duplication of logic between the component and the template.
As the form grows, this structure holds. Additional fields introduce additional schema entries and template bindings, not new subscription logic. Cross-field validation can be added declaratively. Asynchronous validation and persistence can be layered on without rewriting the core model.
Most importantly, the form remains inspectable. At any point during execution, the model signal reflects the current state of the form. Derived state validity, errors, and UI flags can be understood by reading the code, not by tracing runtime behavior.
What we did not solve yet (and why)
At this stage, it would be easy to walk away with the impression that Signal Forms eliminates most of the hard problems associated with form handling. That impression would be misleading. What we have built so far is intentionally incomplete, not because the approach falls short, but because introducing too much too early obscures the value of the underlying model.
One area we have deliberately postponed is cross-field validation that expresses richer business rules. Many real-world forms depend on relationships between fields rather than isolated constraints. Password confirmation is a familiar example, but more complex scenarios quickly arise in enterprise applications. While Signal Forms support these patterns, introducing them before establishing a clear understanding of derived state risks turns validation back into an imperative exercise rather than a declarative one.
We have also avoided asynchronous validation. Server-backed checks introduce latency, partial failure, cancellation, and race conditions. These are not trivial concerns, and treating them casually often leads to subtle bugs and confusing user experiences. Although Signal Forms provide the necessary hooks to model asynchronous behavior, doing so responsibly requires a careful discussion of pending state, effects, and life-cycle boundaries. That discussion belongs in its own article.
Another omission is persistence and synchronization. Many forms need to autosave drafts, synchronize state with local storage, or react to changes by triggering external side effects. These behaviors are not part of the form state itself; they are consequences of state changes. Treating them as such is essential to keeping the architecture comprehensible. Introducing persistence too early would blur the distinction between state and reaction that this article has worked to establish.
Finally, this article has not addressed migration and interoperability. Few teams are starting from a blank slate. Most will adopt Signal Forms incrementally within applications that already rely on reactive forms or template-driven forms. Hybrid approaches, bridging strategies, and gradual refactors are all critical topics, but they presuppose familiarity with both paradigms. Addressing migration before establishing a solid signal-first mental model would undermine that foundation.
These omissions are intentional. A form architecture that tries to do everything at once often ends up doing nothing clearly. By focusing on the core ideas of state, derivation, and declarative validation, we create a base that can absorb additional complexity without collapsing under it.
Signal Forms in the context of Angular’s evolution
To fully appreciate Signal Forms, it helps to step back and view them not as an isolated feature, but as part of a broader shift in Angular’s design philosophy.
For much of its history, Angular emphasized declarative templates paired with imperative coordination in component classes. RxJS became the backbone of that coordination, providing a powerful abstraction for handling asynchronous workflows, user input, and external events. This model scaled well, but it also encouraged developers to express state indirectly through streams and subscriptions.
Signals represent a deliberate recalibration. They re-center Angular’s reactivity model around state and derivation, rather than events and emissions. This shift is visible across the framework: in component inputs, change detection, and now forms. Signal Forms are not an attempt to replace everything that came before; they are an attempt to make the most common use case, modeling and deriving state, simpler and more explicit.
Framed this way, the design of Signal Forms aligns more closely with state-driven form behavior. The requirement to start with a model signal reflects the idea that the state should have a single, inspectable source of truth. Schema-based validation aligns with the notion that constraints are properties of state, not behaviors triggered by events. Field state exposed as signals reinforces the idea that validity, errors, and interaction metadata are derived values that should be read, not managed.
It is also worth noting that Signal Forms do not attempt to abstract away form behavior. They do not hide form state behind opaque classes or life-cycle hooks. They do not require developers to think in terms of control hierarchies or subscription graphs. Instead, they expose form behavior directly, making it easier to reason about how values, validation, and UI feedback relate to one another.
This approach aligns closely with other recent changes in Angular, including the introduction of modern template control flow and a stronger emphasis on explicit data dependencies. Together, these features point toward a framework that favors clarity over indirection and composition over orchestration.
Importantly, Signal Forms are still evolving. Their APIs may change, and their surface area will almost certainly expand. That is precisely why grounding them in first principles matters. Developers who understand why Signal Forms work the way they do will be far better equipped to adapt as the APIs mature.
This article has intentionally avoided duplicating documentation or enumerating every available feature. Instead, it has focused on establishing a conceptual framework that makes the official APIs feel intuitive rather than surprising. When viewed this way, Signal Forms are not a new way to write forms; they are a clearer expression of what forms have always been.
A new way to think about forms
Building the registration form in this article reveals a quiet but important shift. The reduction in complexity does not come from fewer features or simpler requirements. It comes from expressing form behavior in terms of state and derivation rather than orchestration and reaction.
By treating the data model as the single source of truth, validation rules as declarative constraints, and UI behavior as derived from current conditions, much of the coordination logic that typically surrounds forms becomes unnecessary. There are fewer subscriptions to manage, fewer flags to synchronize, and fewer life-cycle concerns to reason about. Form behavior becomes easier to inspect because it is visible directly in the relationships between values.
This approach does not eliminate the hard problems associated with forms. Asynchronous validation, persistence, and interoperability with existing Angular Forms APIs still require careful design. What changes is where that complexity lives. Instead of being interwoven with state representation, those concerns are layered explicitly on top of a clear foundation.
Signal-first forms are not a universal replacement for existing patterns, nor are they a shortcut to simpler applications. They are, however, a strong example of how aligning APIs with first principles can reduce cognitive overhead and improve maintainability over time. For teams building large, state-heavy forms, this alignment can make the difference between code that merely works and code that continues to evolve without friction.
Most generative AI tools know less about R than languages like JavaScript and Python, thanks to how much training data is available for each. However, with a little extra setup, you can give a large language model (LLM) the knowledge it needs to improve its R skills.
Here are 10 ways to help generative AI write R code like a pro.
Use a coding agent
AI coding agents have more power, flexibility, and coding-focused tools than general-purpose chatbots.
Anthropic’s Claude Code and OpenAI’s Codex agents have versions that run in a terminal, IDE extensions, desktop and mobile apps, and other integrations.
R users may also be interested in Posit’s Posit Assistant, which is designed for data analysis in both R and Python. It needs less setup for R than general-purpose coding agents, and it has more built-in knowledge about data science, R package development, and Shiny apps. Plus, it can read objects in your R and Python environments by default, which can be useful in some situations (although perhaps not if you’re working with sensitive data). There are versions of Posit Assistant for RStudio, the Positron IDE, and the terminal. Setup info is here.
Note: Posit Assistant is not the same as the older Positron Assistant that was demo’d at last year’s posit::conf(). The older assistant will eventually be superceded by Posit Assistant. Posit Assistant comes pre-installed with RStudio but not pre-activated, so you don’t need to use it unless you want to.
Google’s original entry in this space, Gemini CLI, is being retired in favor of a new “Antigravity CLI” starting June 18.
There are other IDE platforms with built-in agents, including Cursor and Windsurf, and terminal + subscription coding agents such as Warp, which was open-sourced in April. However, I won’t be covering those here.
You may be limited at work by what your employer allows you to use, but you can still experiment on your own with public data — and then lobby for a new agent if a different one better meets your needs. Whatever you choose, though, moving to one of these from a web-based chatbot should produce better code.
“The shift from chatbot to agent is the most important change in how people use AI since ChatGPT launched,” says Ethan Mollick, co-director of the Wharton School’s Generative AI Labs at the University of Pennsylvania.
Set up CLAUDE.md, AGENTS.md, or GEMINI.md knowledge files
All the major coding agents look for knowledge files to load each time you start a session, and you can easily edit those files. The files can include things like your proficiency in various languages and how you like to document projects. For example, I tell my LLMs that I’d like a README.md in every project with info on how to use the project and technical info on how the project was coded.
Most coding tools let you have both an overarching main file that’s loaded into every session, and an additional file per project.
If you’re adding Claude or Codex to an existing code base, the /init slash command will create one of those .md project files after scanning and analyzing the code. You can also ask an LLM to interview you to create or add to one of those files.
Claude Code looks for a user-level main CLAUDE.md file at ~/.claude/CLAUDE.md and then a project-level file at ./CLAUDE.md or ./.claude/CLAUDE.md. The /memory slash command shows where those files are located. Codex uses AGENTS.md files. Antigravity docs say it looks for a main GEMINI.md file plus GEMINI.md and/or AGENTS.md files in your working directory.
Positron Assistant is set up to read AGENT.md, AGENTS.md, POSITRON.md, CLAUDE.md, GEMINI.md, and LLMS.txt in a workspace root directory, according to the docs. See the Positron docs for more on custom instructions.
Use agent skills
Agent skills can turn an LLM with questionable R knowledge into an expert that understand how you want it to write code. You can develop your own skills, work with your LLM to create skills, or download skills written by others. Claude even has a skill to help you build skills. This is one of the best ways to stop having to repeat the same instructions over and over. Instead, the LLM will “remember” what’s in your skills files by loading them when they’re needed.
What’s the difference between the knowledge .md files mentioned above and a skill? I like to think of CLAUDE.md or AGENTS.md files as important background info that’s always available, while skills are more specific instructions that are useful at certain times. Or, as Claude Opus 4.7 put it: “Skills are for triggered workflows (‘when user asks X, do Y’). A standing rule that applies to every project isn’t trigger-based; it’s a directive I should always follow.” Standing rules should go in CLAUDE.md.
“Create a skill when you keep pasting the same instructions, checklist, or multi-step procedure into chat, or when a section of CLAUDE.md has grown into a procedure rather than a fact,” according to Anthropic’s Claude Code docs. “Unlike CLAUDE.md content, a skill’s body loads only when it’s used, so long reference material costs almost nothing until you need it.” Only skill descriptions are loaded at the start of each session, so the LLM knows what skills are available.
Anthropic originally created skills for Claude in October 2025 and then published the concept as an open standard a couple of months later. Most coding agents now use them.
Anthropic has an easy-to-folllow guide to creating custom skills here if you want to try your hand at building your own.
Posit Assistant comes with several R-related skills built in and allows users to add more. You’ll find them here.
If you’re not using Posit Assistant, you can add Posit-developed skills to another coding agent. Posit published a GitHub repository with skills for creating R packages, Shiny apps, and Quarto documents; for creating and resolving GitHub pull requests; for open-source R and Python package releases; for code reviews (R, Python, JavaScript/TypeScript, SQL); and for creating architectural documentation for a codebase. Browse them all here.
Read and edit the skills you download
Skills are just a folder with one or more markdown text files that have a structured YAML header. That folder can include optional resources and scripts, too. Skill instruction files are easy to read and edit, so you can make sure they do what you want. Tweak them as desired.
For example, Claude helped me write my own customized R skill. Claude leans toward tidyverse packages, but there are other packages I like a lot, too, such data.table. I made sure the R skill knows my package preferences, and under what circumstances I prefer each.
If one of your preferred procedures changes, remember to update the associated skills. Or, ask an LLM to update it for you. Keeping skills up to date will make your coding agent more useful.
Keep your CLAUDE.md or AGENTS.md files updated, too. Maybe you started off with a CLAUDE.md file that said you’re an experienced Python programmer who is just learning R. Three months from now, you might want to update your R skill level. Or, perhaps you told an AGENTS.md file that you like thoroughly documented code, but now you’d prefer a lighter touch. You may want to have your coding agent go over those files with you from time to time and ask you about the main points and whether they need updating.
Use the btw R package and its MCP server
It’s frustrating when an LLM writes code based on an ancient R package version in its training data, or when it doesn’t know about a relatively recent package at all. btw solves this problem by letting your coding agent access info about all the R packages installed on your system. That means it can write code based on your specific R environment. Plus, btw lets an LLM access variables in your current R environment via a Model Context Protocol (MCP) server.
MCP servers are a standardized way for LLMs to access external data — in this case, your running R session (external doesn’t have to mean cloud). Like skills, MCP was created by Anthropic as an open standard and has since been adopted by most major AI platforms. The btw R package includes an MCP server, which you can install for Claude Code by running the following code in a terminal window (not R console).
claude mcp add -s "user" r-btw -- Rscript -e "btw::btw_mcp_server()"
btw comes with a lot of tools. It’s not a great idea to overload your LLM with tools it may never use. If you just want to register btw tools for accessing your R session and looking up package documentation, you can run a command like this instead:
To connect Claude Code to your open R session, you also need to run btw::btw_mcp_session()in every new R session. (I sometimes forget this part until I’m puzzled why Claude can’t read my R variables or even see what packages I have installed.)
You can see a demo of btw in this Posit video that compares Claude Code and Posit Assistant.
The btw R package lets Claude Code and other agents read objects in your R session.
Foundry
You don’t need to do this setup for Posit Assistant, since tools for accessing your R session and variables are built in.
Use plan mode for your projects
I cannot stress this enough. Just as humans benefit from having a plan before starting to implement a project, LLMs benefit from working through a plan before beginning to generate code. Plus, it can be useful at times to “brainstorm” with an LLM about structures and features, ask it for alternatives, or even have another LLM review a plan before implementing it.
Anthropic advises starting with plan mode. “Letting Claude jump straight to coding can produce code that solves the wrong problem. Use plan mode to separate exploration from execution,” they wrote in a best practices doc.
Claude, Codex, Gemini, and Posit Assistant all have a plan mode in their CLI versions that can be activated with the /plan slash command.
Foundry
When in plan mode activated with /plan, Claude will often ask questions before coming up with its plan.
Foundry
Make sure your coding agent learns from its mistakes
This is one of the best tips I read first from Joe Amditis, associate director of operations at the Center for Cooperative Media at Montclair State University: If your coding agent makes a mistake and you work together to fix it, make sure to save that memory so it doesn’t make the same error again.
Your agent may have an auto memory system that’s supposed to do some of this (I’ve used Claude Code’s), but you can instruct any coding agent to keep a separate lessons learned file for each project, too, or ask it what’s the best way to ensure it doesn’t make the same error again. This isn’t a guarantee, any more than you can be assured an LLM will follow all the rules in a CLAUDE.md or AGENTS.md file or all the steps in a skill. But it has definitely cut down on my agents making the same errors over and over.
Have your agent write tests and do code reviews
Tests and code reviews done by a coding agent aren’t a substitute for a human checking that software works. Review an LLM’s results yourself any time a task is important! However, agent-generated tests are still a good way to head off some problems and cut down the time you spend as your LLM’s debugging partner. If you don’t know where to start to help your agent know how to test and review code, see Posit’s testing-r-packages and critical-code-reviewer skills. Your agent may also have built-in code review skills, such as Claude Code’s /code-review slash command.
Code review skills are available from many sources. For example, Sentry has a code review skill in its repository. Aimed specifically at its project’s needs, it can be a useful sample for similar skills. And superpowers is a popular set of general-purpose programming skills that’s billed as “a complete software development methodology for your coding agents, built on top of a set of composable skills and some initial instructions that make sure your agent uses them.” I don’t use superpowers, but a lot of other people clearly do given its more than 227,000 GitHub stars and 20,000 forks.
Another tip: Several experts suggest using a different LLM — maybe even from a different provider — to conduct a code review, since each model has its own strengths and weaknesses.
Don’t forget general prompting good habits
Whatever programming language you’re using, being clear will improve your outputs. Keep your requests targeted, and don’t expect the LLM to read your mind.
“Codex handles complex work better when you break it into smaller, focused steps,” OpenAI says in its developer docs. “Smaller tasks are easier for Codex to test and for you to review. If you’re not sure how to split a task up, ask Codex to propose a plan.”
“Take time to make your prompts as relevant as possible, just as you would when helping a new teammate scope a task,” Google advises in its Five Best Practices for Using AI Coding Assistants. “Consider what details you need to share for a person to succeed, and provide all those details to your AI tool.”
And, don’t load up your context window to the LLM’s published limit. Performance often degrades as you get close to the maximum token limit.
Take advantage of open LLMs, particularly if you have budget and token limits
Open-weight models — especially ones small enough to run on a desktop computer — may never rival frontier LLMs like Claude, GPT, or Gemini. But for a lot of R coding, they don’t need to.
To see how well an LLM can write R code, it should have the same kind of harness that Claude Code, Codex, or Posit Assistant gives larger LLMs. “The harness is the product,” argues Mallory Mejias at Sidecar, an AI education company. “The model is the engine inside it — important, but increasingly interchangeable.” I wouldn’t go that far, since I still find the LLM to be a critical piece of this equation, and models differ. But the same LLM will perform differently depending on the tools and context it has.
Posit recently added Google’s open-source Gemma 4 26B to Assistant’s existing options of larger, closed-source LLMs. “Up until this point, models of this size — small enough to run comfortably on high-end consumer laptops — were on our radar but not yet capable enough to drive an agent harness like Posit Assistant,” Posit senior software engineer Simon Couch wrote on the Posit blog. “This has changed in the last few months with releases like Gemma 4.” However, to use it in Posit Assistant in RStudio, you still need to run it through a Posit AI subscription, not locally. It uses 1/10 the budget that the same session with Claude Sonnet would consume.
Several projects can use Claude Code to run local LLMs. Ollama, open-source software for running LLMs locally, does this with the terminal command ollama launch claude --model , such as:
ollama launch claude --model gemma4:26b
The makers of Unsloth, an open-source framework for running and training models, say running local LLMs inside Claude Code can be very slow due to a cache invalidation issue. They say this can be fixed by adding "CLAUDE_CODE_ATTRIBUTION_HEADER" : "0" to ~/.claude/settings.json under “env”.
Unsloth can also use Claude Code to run local models, but it’s somewhat more involved to install and set up than Ollama. See this Unsloth tutorial.
Open-source coding agent Open Code runs LLMs from OpenAI, Anthropic, Google, Kimi, Alibaba’s Qwen, and others. I haven’t warmed up to it yet compared with commercially backed CLI tools I’ve tried, but it has 174,000 GitHub stars so clearly a lot of fans.
AI consultant Thomas Wiegold, who switched from Claude to Open Code, said “OpenCode covers everything I need for my daily workflow. It’s fast, the provider flexibility is genuinely useful rather than theoretical, and the TUI is better for extended sessions…. That said, I’m not going to pretend it’s all smooth sailing.” He noted some stability issues and a remote code execution vulnerability earlier this year.
Large language models remain imperfect and unpredictable tools, but they’re improving rapidly — as are the harnesses around them. Even frontier LLMs in commercial coding agents can ignore instructions at times and otherwise behave in unpleasantly surprising ways. However, you’ll vastly improve your chances of generating quality code if you use coding agents, take time to set them up with quality instructions, and remember good prompting techniques. Good luck!
—
Coding agent info
Claude Code – Anthropic’s coding agent is available in the terminal and as an IDE extension, a desktop app, mobile apps, Slack app, and a cloud version that can work directly in your repos on GitHub. Arguably the first breakthrough coding agent, it now includes an elegant remote-control option where you can start a session on your desktop and then continue it on a phone or tablet. While designed for Claude LLMs, you can use it to run local LLMs via tools like Ollama or Unsloth Studio. When using Claude models, it tends to offer less usage than other vendors’ options. Anthropic has posted the terminal version’s code on GitHub, with usage governed by Anthropic’s commercial terms of service. You can use Claude Code via API pay per use or as part of a Claude subscription.
Codex – OpenAI’s answer to Claude Code is available via terminal, app, IDE extension, or cloud, with integrations for GitHub, Slack, and Linear. The CLI tool is open source under an Apache license, available on GitHub, and works via API pay per use or as part of a ChatGPT subscription.
Posit Assistant – The Posit (formerly RStudio) coding agent is designed for data work in R and Python, although Posit senior software engineer Simon Couch told me it would do fine for programming work for other uses and in other languages. It is available for the RStudio IDE (via a Posit AI subscription in RStudio), Positron IDE, and as a stand-alone CLI terminal app, although the CLI tool currently doesn’t feel as full-featured as the IDE integrations (that may change). As of this writing, you can also use API keys from Anthropic, OpenAI, and Snowflake Cortex and a GitHub Copilot account in Positron. Posit Assistant is being updated quite frequenty, Couch told me. If you tested it a month or two ago, it has more capabilities now.
Antigravity CLI – Google’s terminal-based tool will be replacing the Gemini CLI for unpaid and Google One users this month. There’s also an Antigravity IDE, SDK, and platform for orchestrating multiple agents. One advantage of Antigravity: Google offers a free plan with access to Gemini 3.5 Flash, Gemini 3.1 Pro, Gemini 3 Flash, Claude Sonnet 4.6, Claude Opus 4.6, and gpt-oss-120b, and what it describes as “generous” but undefined weekly rate limits based on “the degree we have capacity.”
Between 2029 and 2032, every currently supported long-term support (LTS) version of Java will reach end-of-support within a single three-year window: Java 17 in 2029, Java 8 in 2030, Java 21 in 2031, and Java 11 in 2032.
On paper, this looks like a manageable upgrade cycle. In practice, it creates a collision of timelines that most enterprises have failed to forecast. Organizations attempting to modernize incrementally—moving application by application, version by version—are operating on a model that the calendar has already rendered obsolete.
The primary danger here is the illusion of time. Traditional modernization plans rely on sequential upgrades and controlled pacing. However, when every major Java version expires in the same compressed window, sequential planning collapses. By the time this becomes obvious, organizations will be forced into reactive mode, making rushed decisions under extreme pressure.
The modernization illusion
For organizations planning traditional stepwise upgrades—Java 8 to Java 11 to Java 17 to Java 21—this convergence elevates a routine maintenance task into a structural crisis. Enterprises with large Java estates will be forced to upgrade multiple applications across multiple versions simultaneously to maintain security compliance and business continuity. Waiting until the late 2020s to act guarantees a modernization process under emergency conditions.
While modern Java versions maintain strong backward compatibility, they cannot offset the drag of what enterprises are carrying forward: decades of accumulated technical debt.
In large Java environments, technical debt is pervasive. It exists as unused libraries, obsolete logic, forgotten dependencies, and dormant features—quietly inflating the size, risk, and complexity of every modernization effort. In many organizations, a significant portion of the codebase no longer executes in production, yet it still consumes developer attention, security oversight, and planning effort.
As codebases grow older and larger, this drag compounds. What looks like a simple version upgrade on a roadmap becomes a massive operational burden in practice.
Why incremental planning fails
Most modernization strategies assume that upgrades can be sequenced and absorbed gradually. That assumption is now dangerous. When multiple Java versions reach end-of-support in the same narrow window, enterprises don’t face a single modernization project—they face parallel modernization across their entire estate.
This shifts the challenge from engineering complexity to organizational capacity.
Consider a typical enterprise with 100 developers. If even a fraction of their time is spent maintaining, investigating, or working around unused and obsolete code, the organization burns meaningful engineering capacity on work that delivers no business value. Multiply that across dozens or hundreds of applications, and the bottleneck becomes clear: modernization is limited by people, not frameworks.
Parallel modernization requires parallel capacity—something most organizations haven’t budgeted for.
This explains why traditional approaches struggle to scale. Tools that analyze code in isolation cannot distinguish what actually matters in production. Without clear visibility into what code is relevant, organizations default to caution, effectively converting their timelines into risk.
The real bottleneck: developer capacity
The Java modernization crunch is a crisis of resource allocation, not a technology problem.
Every hour developers spend maintaining obsolete code or investigating unused dependencies is an hour lost to modernization. When organizations face simultaneous upgrades across multiple applications, human capacity becomes the limiting factor. Sequential planning and parallel modernization require the time and capacity most enterprises no longer have.
Organizations that delay action are consuming their flexibility rather than preserving it. Each year of inaction increases the volume of code that must be moved, reviewed, secured, and modernized within the same fixed window. By the time deadlines become unavoidable, the only remaining options are compression, shortcuts, and uncomfortable trade-offs.
A different way to think about readiness
The organizations that navigate this transition successfully will prioritize clarity over immediate upgrades.
Modernization at scale requires an accurate understanding of what actually matters in production before attempting to move it forward. Without that visibility, every upgrade effort inherits unnecessary complexity, consumes excess capacity, and introduces avoidable risk.
The goal is not simply adopting better tools, but reducing the structural load enterprises carry into modernization. Leaner systems modernize faster. Simpler estates scale better. Complexity compounds under time pressure.
The timeline is already set
The Java modernization crunch is a timing problem that is already locked in.
Enterprises that treat the next few years as business-as-usual will discover that sequential plans cannot survive compressed timelines. Those that confront technical debt now—before the pressure hits—will find the coming transition difficult but manageable. Those that don’t will face rushed decisions and permanent trade-offs.
By the time 2029 arrives, the window for gradual modernization will have closed. The calendar won’t wait for us to be ready.
—
New Tech Forum provides a venue for technology leaders—including vendors and other outside contributors—to explore and discuss emerging enterprise technology in unprecedented depth and breadth. The selection is subjective, based on our pick of the technologies we believe to be important and of greatest interest to InfoWorld readers. InfoWorld does not accept marketing collateral for publication and reserves the right to edit all contributed content. Send all inquiries to doug_dineley@foundryco.com.
Of all the reasons Python is a hit with developers, one of the biggest is its broad and ever-expanding selection of third-party packages. Convenient toolkits for everything from ingesting and formatting data to high-speed math and machine learning are just an import or pip install away.
But what happens when those packages don’t play nice with each other? What do you do when different Python projects need competing or incompatible versions of the same add-ons? That’s where Python virtual environments come into play.
What are Python virtual environments?
A virtual environment is a way to have multiple, parallel instances of the Python interpreter, each with different sets of packages and different configurations. Each virtual environment contains a discrete copy of the Python interpreter, including copies of its support utilities (such as the package manager pip).
The packages installed in each virtual environment are seen only in that virtual environment and no other. Even large, complex packages with platform-dependent binaries can be corralled off from each other in virtual environments.
Why use Python virtual environments?
There are a few common use cases for a virtual environment:
You’re developing multiple projects that depend on different versions of the same packages, or you have a project that must be isolated from certain packages because of a namespace collision. This is the most standard use case.
You’re working in a Python environment where you can’t modify the site-packages directory. This may be because you’re working in a highly controlled environment, such as managed hosting, or on a server where the choice of interpreter (or packages used in it) can’t be changed because of production requirements.
You want to experiment with a specific combination of packages under highly controlled circumstances, for instance to test cross-compatibility or backward compatibility.
You want to run a “baseline” version of the Python interpreter on a system with no third-party packages, and only install third-party packages for each individual project as needed.
Nothing says you can’t simply unpack a Python library into a subfolder of a project and use it that way. Likewise, you could download a standalone copy of the Python interpreter, unpack it into a folder, and use it to run scripts and packages devoted to it.
But managing such cobbled-together projects soon becomes difficult. It only seems easier to do that at first. Working with packages that have binary components, or that rely on elaborate third-party dependencies, can be a nightmare. Worse, reproducing such a setup on someone else’s machine, or on a new machine you manage, is tricky.
The best long-term solution is to use Python’s native mechanisms for creating, reproducing, and working with virtual environments.
How to use virtual environments in Python 3
Python has native tooling for virtual environments that makes the whole process quite simple. This wasn’t always the case, but now all supported versions of Python use the native virtual environment tool, venv.
Create the Python virtual environment
To create a virtual environment in a given directory, type:
python3 -m venv /path/to/venv
For instance, to create the virtual environment in the current directory, using the subdirectory .venv type:
python3 -m venv .venv
On Microsoft Windows, you can use py instead of python3 to reliably access an installed Python version. (See this article for more about using the py launcher in Windows.)
The exact name for the virtual environment directory is arbitrary, but it’s typically .venv.
The whole process of setting up the virtual environment may take a minute or two. When it’s finished, you should see that directory with a few subdirectories in it. The most important subdirectory is bin on Unix or Scripts on Windows, which is where you’ll find the copy of the Python interpreter for the virtual environment along with its utilities.
Note that because each virtual environment contains its own copy of the Python interpreter, it can be fairly large. A Python 3.13 virtual environment will consume anywhere from 14MB to 26MB of disk space, depending on the operating system.
Venvs and version control
If you are setting up a virtual environment in a project directory managed with some kind of version control system (e.g., Git), exclude the environment directory from tracking before you make any commits after creating the venv.
Venvs should not be tracked along with their associated code, as they are meant to be destroyed and recreated as needed. You should, however, track the requirements.txt or pyproject.toml files associated with the project, as those are used to describe what gets installed in the venv for that project.
Activate the Python virtual environment
Before you can use this virtual environment, you need to explicitly activate it. Activation makes the virtual environment the default Python interpreter for the duration of a shell session.
You’ll need to use different syntax for activating the virtual environment depending on which operating system and command shell you’re using.
On Unix or MacOS, using the bash shell: source /path/to/venv/bin/activate
On Unix or MacOS, using the csh shell: source /path/to/venv/bin/activate.csh
On Unix or MacOS, using the fish shell: source /path/to/venv/bin/activate.fish
On Windows using the Command Prompt:path/to/venv/Scripts/Activate.bat
On Windows using PowerShell: path/to/venv/Scripts/Activate.ps1
Note that the activated environment only works for the context it was activated in. For instance, if you launch two instances of PowerShell, A and B, and you activate the virtual environment in instance A, that environment will apply only to A, not B.
Many Python IDEs will automatically detect and activate a virtual environment if one is found in the current project directory. Visual Studio Code, for instance, can do this when the Python extension is enabled. Opening a terminal inside Visual Studio Code will automatically activate the selected virtual environment. PyCharm automatically creates a virtual environment for each new project and enables it automatically.
Configure and use the Python virtual environment
Once you’ve activated the new virtual environment, you can use the pip package manager to add and change packages for it. You’ll find pip in the Scripts subdirectory of the virtual environment on Windows, and in the bin subdirectory on Unix OSes.
If you’re already familiar with the way pip works, you’re set. It should be just the same in a virtual environment. Just make sure you’re using the instance of pip that manages packages for the virtual environment in the context where it was activated—e.g., the bash session or Windows CLI/PowerShell session. If you want to verify that you’re using the right pip and the right virtual environment, type pip -V and check that the path it displays points to a subdirectory of your virtual environment.
Note that when you want to upgrade pip in a virtual environment, it’s best to use the command python3 -m pip install -U pip. This ensures the upgrade process is run in such a way that Python doesn’t lock crucial files. The command pip install -U pip may not be able to complete the upgrade properly.
To use the virtual environment you created to run Python scripts, simply invoke Python from the command line in the context where you activated it. For instance, to run a script, just run python3 myscript.py.
When you create a new virtual environment, pip will be installed, but that’s all. You’ll need to install any other packages you want to use in the environment. For projects with complex requirements, it is customary to keep in the root of the project a requirements.txt file that lists the requirements for the project. This way, if you need to recreate the virtual environment, you can reinstall all of the needed packages with the command pip install -r requirements.txt.
More recently, a new project metadata format has emerged for Python projects, called pyproject.toml. A pyproject.toml file contains the package requirements of the project, but also a great deal of other information about it. To install those requirements, you’d run pip install . in the same directory as the pyproject.toml file.
Note that the copy of pip that lives in a virtual environment is local to that virtual environment. Each virtual environment has its own copy, which will need to be updated and maintained independently. This is why you may get warnings about pip being out of date in some virtual environments but not others; pip has to be updated in each virtual environment separately.
Deactivating the Python virtual environment
When you’re done using the virtual environment, you can just terminate the session where you were using it. If you want to continue to work in the same session but with the default Python interpreter instead, type deactivate at the prompt. Windows users on the Command Prompt need to run deactivate.bat from the Scripts subdirectory, but Unix users and Windows users running PowerShell can simply type deactivate in any directory.
Removing the Python virtual environment
Virtual environments are self-contained. When you no longer need the virtual environment, you can simply delete its directory. Just make sure you first close any running copies of Python that use the virtual environment.
Whenever you want to refresh or recreate a virtual environment, you can simply delete the current environment directory and recreate the venv as described above (see “Managing packages in Python virtual environments”). This is typically the easiest way to upgrade a project to a newer version of Python: delete or temporarily re-name the venv directory (in case the new version doesn’t work), then create a new one and install the project requirements into it.
If you have many older projects that aren’t being actively used, you can remove their environments to save space. They can be recreated easily whenever needed, although you will want to note which version of Python was used to create them along with the needed packages (typically recorded in requirements.txt or pyproject.toml).
Relocating the Python virtual environment
It’s tempting to assume a virtual environment can be copied and moved around along with its project. Don’t do this. Virtual environments are tied to the location of the Python installation on the system where they’re created. If you want to move the project to another system, leave out the venv directory, and recreate the venv on the target machine. Do copy and move the requirements.txt or pyproject.toml file with the project, because those files are needed to recreate the venv on the other system.
How to use virtual environments in Python 2
With Python 2, virtual environments aren’t a native feature of the language. Instead, you need to install third-party libraries to create and manage virtual environments.
The most popular and widely used of these projects is virtualenv, which handles creating the directory structure and copying the needed files into a virtual environment. To install virtualenv, just use pip install virtualenv. To create a virtual environment directory with it, type virtualenv /path/to/directory. Activating and deactivating the virtual environment works the same way as it does for virtual environments in Python 3 (see above).
Note that Python 2 should not be used for any new development. Virtual environments in Python 2, like Python 2 itself, should be used only for the maintenance of legacy projects that should eventually be migrated to Python 3.
Virtual environments and the main Python package directory
Normally, when you create a venv, it cannot work with the packages already present in the Python installation that created it. This is by design, as it ensures that packages installed globally don’t interfere with local ones.
You can override this behavior, but only when you first create a virtual environment. If you pass the flag --system-site-packages to venv when you run it, the created venv will have access to the parent Python’s package directory.
Using Python virtual environments with Jupyter notebooks
If you’re using Jupyter notebooks (aka IPython notebooks), and you already have Jupyter installed systemwide, create your virtual environment and activate it. Then, from your virtual environment directory, run pip install ipykernel to add the needed components for IPython. Finally, run ipython kernel install —user —name=, where project_name is a name you want to associate with that particular project. From there you should be able to launch Jupyter and switch to the IPython kernel you installed inside the virtual environment.
Upgrading Python virtual environments
When you upgrade a Python runtime on your system, virtual environments that use that version of Python aren’t automatically upgraded. That’s your responsibility. And that’s by design, because unwitting upgrades to Python versions can break their attendant packages.
If you’ve upgraded an existing Python interpreter with a minor point upgrade—e.g., from Python 3.13.1 to Python 3.13.3—you can upgrade any corresponding virtual environments easily enough. From a command prompt in the project directory, enter:
python -m venv /path/to/venv --upgrade
Don’t activate the virtual environment beforehand, or the upgrade may not work.
Alternatively, as noted above (see “Removing the Python virtual environment”), you could elect to remove the venv completely and recreate it using your requirements.txt or pyproject.toml file.
If you’ve installed a major new version of Python—e.g., you already have Python 3.10 and you now install Python 3.11 alongside it—you’ll need to create a new virtual environment that specifically uses the new major point version. Do not attempt to upgrade an existing virtual environment to a higher major point version of Python.
Of all the reasons Python is a hit with developers, one of the biggest is its broad and ever-expanding selection of third-party packages. Convenient toolkits for everything from ingesting and formatting data to high-speed math and machine learning are just an import or pip install away.
But what happens when those packages don’t play nice with each other? What do you do when different Python projects need competing or incompatible versions of the same add-ons? That’s where Python virtual environments come into play.
What are Python virtual environments?
A virtual environment is a way to have multiple, parallel instances of the Python interpreter, each with different sets of packages and different configurations. Each virtual environment contains a discrete copy of the Python interpreter, including copies of its support utilities (such as the package manager pip).
The packages installed in each virtual environment are seen only in that virtual environment and no other. Even large, complex packages with platform-dependent binaries can be corralled off from each other in virtual environments.
Why use Python virtual environments?
There are a few common use cases for a virtual environment:
You’re developing multiple projects that depend on different versions of the same packages, or you have a project that must be isolated from certain packages because of a namespace collision. This is the most standard use case.
You’re working in a Python environment where you can’t modify the site-packages directory. This may be because you’re working in a highly controlled environment, such as managed hosting, or on a server where the choice of interpreter (or packages used in it) can’t be changed because of production requirements.
You want to experiment with a specific combination of packages under highly controlled circumstances, for instance to test cross-compatibility or backward compatibility.
You want to run a “baseline” version of the Python interpreter on a system with no third-party packages, and only install third-party packages for each individual project as needed.
Nothing says you can’t simply unpack a Python library into a subfolder of a project and use it that way. Likewise, you could download a standalone copy of the Python interpreter, unpack it into a folder, and use it to run scripts and packages devoted to it.
But managing such cobbled-together projects soon becomes difficult. It only seems easier to do that at first. Working with packages that have binary components, or that rely on elaborate third-party dependencies, can be a nightmare. Worse, reproducing such a setup on someone else’s machine, or on a new machine you manage, is tricky.
The best long-term solution is to use Python’s native mechanisms for creating, reproducing, and working with virtual environments.
How to use virtual environments in Python 3
Python has native tooling for virtual environments that makes the whole process quite simple. This wasn’t always the case, but now all supported versions of Python use the native virtual environment tool, venv.
Create the Python virtual environment
To create a virtual environment in a given directory, type:
python3 -m venv /path/to/venv
For instance, to create the virtual environment in the current directory, using the subdirectory .venv type:
python3 -m venv .venv
On Microsoft Windows, you can use py instead of python3 to reliably access an installed Python version. (See this article for more about using the py launcher in Windows.)
The exact name for the virtual environment directory is arbitrary, but it’s typically .venv.
The whole process of setting up the virtual environment may take a minute or two. When it’s finished, you should see that directory with a few subdirectories in it. The most important subdirectory is bin on Unix or Scripts on Windows, which is where you’ll find the copy of the Python interpreter for the virtual environment along with its utilities.
Note that because each virtual environment contains its own copy of the Python interpreter, it can be fairly large. A Python 3.13 virtual environment will consume anywhere from 14MB to 26MB of disk space, depending on the operating system.
Venvs and version control
If you are setting up a virtual environment in a project directory managed with some kind of version control system (e.g., Git), exclude the environment directory from tracking before you make any commits after creating the venv.
Venvs should not be tracked along with their associated code, as they are meant to be destroyed and recreated as needed. You should, however, track the requirements.txt or pyproject.toml files associated with the project, as those are used to describe what gets installed in the venv for that project.
Activate the Python virtual environment
Before you can use this virtual environment, you need to explicitly activate it. Activation makes the virtual environment the default Python interpreter for the duration of a shell session.
You’ll need to use different syntax for activating the virtual environment depending on which operating system and command shell you’re using.
On Unix or MacOS, using the bash shell: source /path/to/venv/bin/activate
On Unix or MacOS, using the csh shell: source /path/to/venv/bin/activate.csh
On Unix or MacOS, using the fish shell: source /path/to/venv/bin/activate.fish
On Windows using the Command Prompt:path/to/venv/Scripts/Activate.bat
On Windows using PowerShell: path/to/venv/Scripts/Activate.ps1
Note that the activated environment only works for the context it was activated in. For instance, if you launch two instances of PowerShell, A and B, and you activate the virtual environment in instance A, that environment will apply only to A, not B.
Many Python IDEs will automatically detect and activate a virtual environment if one is found in the current project directory. Visual Studio Code, for instance, can do this when the Python extension is enabled. Opening a terminal inside Visual Studio Code will automatically activate the selected virtual environment. PyCharm automatically creates a virtual environment for each new project and enables it automatically.
Configure and use the Python virtual environment
Once you’ve activated the new virtual environment, you can use the pip package manager to add and change packages for it. You’ll find pip in the Scripts subdirectory of the virtual environment on Windows, and in the bin subdirectory on Unix OSes.
If you’re already familiar with the way pip works, you’re set. It should be just the same in a virtual environment. Just make sure you’re using the instance of pip that manages packages for the virtual environment in the context where it was activated—e.g., the bash session or Windows CLI/PowerShell session. If you want to verify that you’re using the right pip and the right virtual environment, type pip -V and check that the path it displays points to a subdirectory of your virtual environment.
Note that when you want to upgrade pip in a virtual environment, it’s best to use the command python3 -m pip install -U pip. This ensures the upgrade process is run in such a way that Python doesn’t lock crucial files. The command pip install -U pip may not be able to complete the upgrade properly.
To use the virtual environment you created to run Python scripts, simply invoke Python from the command line in the context where you activated it. For instance, to run a script, just run python3 myscript.py.
When you create a new virtual environment, pip will be installed, but that’s all. You’ll need to install any other packages you want to use in the environment. For projects with complex requirements, it is customary to keep in the root of the project a requirements.txt file that lists the requirements for the project. This way, if you need to recreate the virtual environment, you can reinstall all of the needed packages with the command pip install -r requirements.txt.
More recently, a new project metadata format has emerged for Python projects, called pyproject.toml. A pyproject.toml file contains the package requirements of the project, but also a great deal of other information about it. To install those requirements, you’d run pip install . in the same directory as the pyproject.toml file.
Note that the copy of pip that lives in a virtual environment is local to that virtual environment. Each virtual environment has its own copy, which will need to be updated and maintained independently. This is why you may get warnings about pip being out of date in some virtual environments but not others; pip has to be updated in each virtual environment separately.
Deactivating the Python virtual environment
When you’re done using the virtual environment, you can just terminate the session where you were using it. If you want to continue to work in the same session but with the default Python interpreter instead, type deactivate at the prompt. Windows users on the Command Prompt need to run deactivate.bat from the Scripts subdirectory, but Unix users and Windows users running PowerShell can simply type deactivate in any directory.
Removing the Python virtual environment
Virtual environments are self-contained. When you no longer need the virtual environment, you can simply delete its directory. Just make sure you first close any running copies of Python that use the virtual environment.
Whenever you want to refresh or recreate a virtual environment, you can simply delete the current environment directory and recreate the venv as described above (see “Managing packages in Python virtual environments”). This is typically the easiest way to upgrade a project to a newer version of Python: delete or temporarily re-name the venv directory (in case the new version doesn’t work), then create a new one and install the project requirements into it.
If you have many older projects that aren’t being actively used, you can remove their environments to save space. They can be recreated easily whenever needed, although you will want to note which version of Python was used to create them along with the needed packages (typically recorded in requirements.txt or pyproject.toml).
Relocating the Python virtual environment
It’s tempting to assume a virtual environment can be copied and moved around along with its project. Don’t do this. Virtual environments are tied to the location of the Python installation on the system where they’re created. If you want to move the project to another system, leave out the venv directory, and recreate the venv on the target machine. Do copy and move the requirements.txt or pyproject.toml file with the project, because those files are needed to recreate the venv on the other system.
How to use virtual environments in Python 2
With Python 2, virtual environments aren’t a native feature of the language. Instead, you need to install third-party libraries to create and manage virtual environments.
The most popular and widely used of these projects is virtualenv, which handles creating the directory structure and copying the needed files into a virtual environment. To install virtualenv, just use pip install virtualenv. To create a virtual environment directory with it, type virtualenv /path/to/directory. Activating and deactivating the virtual environment works the same way as it does for virtual environments in Python 3 (see above).
Note that Python 2 should not be used for any new development. Virtual environments in Python 2, like Python 2 itself, should be used only for the maintenance of legacy projects that should eventually be migrated to Python 3.
Virtual environments and the main Python package directory
Normally, when you create a venv, it cannot work with the packages already present in the Python installation that created it. This is by design, as it ensures that packages installed globally don’t interfere with local ones.
You can override this behavior, but only when you first create a virtual environment. If you pass the flag --system-site-packages to venv when you run it, the created venv will have access to the parent Python’s package directory.
Using Python virtual environments with Jupyter notebooks
If you’re using Jupyter notebooks (aka IPython notebooks), and you already have Jupyter installed systemwide, create your virtual environment and activate it. Then, from your virtual environment directory, run pip install ipykernel to add the needed components for IPython. Finally, run ipython kernel install —user —name=, where project_name is a name you want to associate with that particular project. From there you should be able to launch Jupyter and switch to the IPython kernel you installed inside the virtual environment.
Upgrading Python virtual environments
When you upgrade a Python runtime on your system, virtual environments that use that version of Python aren’t automatically upgraded. That’s your responsibility. And that’s by design, because unwitting upgrades to Python versions can break their attendant packages.
If you’ve upgraded an existing Python interpreter with a minor point upgrade—e.g., from Python 3.13.1 to Python 3.13.3—you can upgrade any corresponding virtual environments easily enough. From a command prompt in the project directory, enter:
python -m venv /path/to/venv --upgrade
Don’t activate the virtual environment beforehand, or the upgrade may not work.
Alternatively, as noted above (see “Removing the Python virtual environment”), you could elect to remove the venv completely and recreate it using your requirements.txt or pyproject.toml file.
If you’ve installed a major new version of Python—e.g., you already have Python 3.10 and you now install Python 3.11 alongside it—you’ll need to create a new virtual environment that specifically uses the new major point version. Do not attempt to upgrade an existing virtual environment to a higher major point version of Python.
There is no ordained path. The hope that we were converging on some kind of consensus in web development has been eradicated by recent, ingenious developments that point in almost every direction. Yet, if there is a central theme uniting these efforts, it is the desire to mitigate the layers of liturgical embellishment that have grown up around the reactive canon. How can we look at things differently to attain the power that we need, without the heavy intricacy?
Here are eight cutting-edge web development tools that point the way.
Front-end maestro
If you put a bunch of classical musicians in a room together with sheet music and let them run, you might get to a cohesive piece—but you probably want a conductor, a maestro who coordinates all of the parts. That is Astro for your front-end frameworks.
Astro addresses the “hydration” of the front end, that is to say, the process of making the shell reactive. In conventional server-side rendering (SSR), like Next.js or Nuxt, the server not only sends the HTML, but also sends the massive framework runtime down the wire, just to attach event listeners to the page. Astro allows you to write components in React, Svelte, Vue, or Solid, and its compiler strips away all of the JavaScript before it reaches the browser. Astro ships zero JS by default, relying on its islands architecture to hydrate only the specific components that demand interactivity.
Because Astro isolates interactivity into distinct islands, sharing complex state between those islands (e.g., a complex filtering sidebar communicating with a separate dynamic data grid) is fundamentally harder than it is in a monolithic single-page application. If you are building a highly interactive, dashboard-heavy app where every component affects every other component, Astro’s isolated islands might begin to feel more like a straitjacket than a liberation.
See also: Qwik. If Astro unbloats by stripping away the JavaScript entirely, Qwik unbloats by delaying it. Qwik delivers instant HTML and serializes the application state, downloading and executing only the JavaScript code required for a specific interaction at the exact millisecond the user clicks a button.
Biome: Lint like it’s 2026
Rust is gradually replacing the underlying infrastructure in the JavaScript ecosystem. But while Rust gives Biome its close-to-the-metal speed, Biome’s true calling card is its unification of the sprawling toolchain under a cohesive umbrella.
The .eslintrc and .prettierrc files and the dozen associated plugins can become a dark and unhappy bog in a project. Biome is the way out of the mire. It is a single, blazingly fast binary that replaces your entire tangled formatting and linting ecosystem, providing a path to code quality that doesn’t require a sprawling web of dependencies.
Probably the biggest drawback to Biome is that you lose the wide-open extensibility—which is exactly the same feature that makes Biome lean.
See also: Rspack. Biome cleans up the linting. Rspack unbloats the build step. Also built on Rust for speed, Rspack challenges the new “unbundled” esbuild-based dev mode championed by Vite and uses bundled dev mode.
Bun: Fast and integrated back-end JavaScript
Most cutting-edge JavaScript enthusiasts are already well-aware of Bun. For those who haven’t yet experienced Bun’s enthralling blend of one-stop shopping and blistering speed first-hand, it’s a virtually irrefutable must-try.
Fast is probably an understatement. If you are used to Node and you try out Bun, you will likely be immediately impressed with the speed at which commands execute. The Bun team has also made an extensive, multi-year effort to bring its engine into close compatibility with Node’s APIs. Overall, Bun is an extraordinary engineering effort that every JS developer should explore.
However, while Bun’s Zig-based engine is in most respects a drop-in for Node, it isn’t perfect, especially when considering the gargantuan landscape of Node packages out there. Node remains the conservative, happy-path engine for server-side JavaScript.
See also: Deno. Although Bun has justifiably earned a reputation for bleeding-edge innovation, Deno has quietly pressed ahead with an appealing set of enterprise features like an integrated deployment platform and a front-end framework (Deno Fresh).
The Bun curious also may want to check out my interview with Bun creator Jared Sumner.
HTMX: Ajax KISS
If we are talking about clever ways to de-complexify the web, HTMX could reasonably be considered the poster-child. It takes the core mechanisms of the modern web client, like Ajax and partial updates, and turns them into simple HTML attributes. That means the state lives exclusively on the server, which is responsible for sending HTMX fragments.
Of course, there are trade-offs. Perhaps most unavoidable is the extreme dependence on the network. Because there is no client-side state machine, the browser will be orphaned and helpless without a connection to the server. That is, unless you get experimental with a local-first datastore.
Long story short: if your app falls into the realm of HTMX’s ability, HTMX is likely to be the most direct RESTful way to build it. And HTMX can in fact handle quite a lot.
See also: Hotwire. A collection of tools for building single-page-style applications using HTML over the wire, Hotwire has great features like page morphing, which can diff HTML instead of cold-loading it, with a simple import. True to classic “free as in speech” software culture, the HTMX and Hotwire projects freely exchange ideas.
PowerSync: Data layer redo
Although the local-first data revolution that PowerSync represents implies a fairly serious engineering deep dive, its core proposal — to entirely reshape the way data moves in web architecture — is something a web developer needs to be aware of.
Usually, we create architectures that require a complex middleware to broker between a reactive client and the datastore. PowerSync proposes a radical alternative: bypass the middleman entirely by dropping a robust SQLite Wasm database directly into the browser.
The UI works on local data using familiar SQL, synchronously. Latency is zero. The dreaded loading spinner vanishes entirely. In the background, PowerSync automatically reconciles your local store with your central Postgres database. It handles the complex syncing algorithms and network drops, effectively making your application offline-first by default.
The catch, of course, is that local-first development forces a massive mental shift. You have to define data slices (similar to a view) that each client user holds. The PowerSync engine does most of the hard work, but things like schema migrations and conflict resolution (when two users edit the same record while offline) require a significantly steeper initial setup than a standard REST API.
See also: RxDB. RxDB is a slightly different flavor of local-first datastore. Whereas PowerSync relies heavily on Postgres, SQLite, and background daemons, RxDB provides a NoSQL, offline-first, reactive database that treats queries as observable streams, pushing UI updates the exact millisecond the local data changes.
RooCode: Use any AI you want
The beauty of RooCode lies in its ability to orchestrate whatever AI providers you have—for free. RooCode is an extension to Visual Studio Code that provides an AI manager layer. This layer bridges between the general abilities of the LLM and your code-specific, project-level structures.
RooCode is strong enough to be somewhat agentic in its capabilities. It doesn’t reach the powerhouse abilities of something like Cursor or Antigravity, but it is quite able to handle most small to medium-sized requests. And it does so with a minimum of unnecessary overhead. I find myself often using RooCode alongside my AI-assisted IDE to knock out lesser requirements, for less cost and without interrupting the flow of ongoing epics.
RooCode keeps you free of proprietary ecosystems. It allows you to plug in your own API keys—whether that is Claude, OpenAI, or even a local model running on your own hardware.
The hidden tax of any AI coding assistant, however, is that it fundamentally shifts your job description from “writer” to “editor.” The unbloating of keystrokes can paradoxically lead to massively bloated codebases if developers blindly accept AI-generated boilerplate without actively reviewing its architectural impact. It is incredibly easy to let an agent spin up 500 lines of complex React when 50 lines of plain JavaScript would have done.
See also: Antigravity. RooCode is a lightweight extension that supercharges your existing environment. Google’s Antigravity is a custom-built editor designed from the ground up around AI, geared for agentic development workflows.
TanStack Query: Syncing made simple(r)
Even when client-side state management is addressed (see Zustand below), there is still a big, gaping hole in the plot: syncing across the server boundary. That is where TanStack Query steps into the breach.
Distributed computing is a notoriously thorny problem, and in fact our standard reactive model walks right into these thorns by holding the same state in two different places: on the client and the server. Tanstack Query tries to make this inherent architectural friction as painless as possible by acting as an intelligent asynchronous layer.
Instead of using a bunch of manual fetches tied to useState updates, along with fragile isLoading flags and complex state synchronization logic, TanStack Query abstracts the heavy lifting of API responses, background updates, and request deduplication into a few elegant hooks. You tell TanStack Query where to get the data, and it uses a pattern known as “stale-while-revalidate,” which means it will cache and reuse data on the front end (eliminating reload waits) and sync to the latest state in the background.
The catch, however, is that cache invalidation remains one of the hardest problems in computer science—and TanStack Query forces you to face it head-on. You will spend time thinking about “query keys” and deciding when a piece of data should be considered “stale.” No free lunches in software.
See also: SWR. While TanStack Query is an absolute powerhouse for complex data manipulation, SWR remains a champion of API minimalism, doing exactly what its name implies (stale-while-revalidate) with almost zero configuration.
Zustand: Minimalist state
If you have yet to encounter the monstrosity of large-scale state management in a reactive app, then spoiler alert: it can be nasty. Zustand proposes to dispense with the ceremonial boilerplate of reducers, providers, and unwieldy context wrappers in favor of a tiny, brutally simple global store.
Instead of forcing your entire application tree into a massive React context provider (sometimes leading to cascades of superfluous re-renders across the DOM), Zustand uses custom hooks to tie state directly to the specific components that need it. Zustand strives to achieve the specificity in the VDOM reactive model (instead of eliminating it entirely a la Signals).
You define a store, you call it, and the reactivity just works. It is an expression of the KISS philosophy applied to front-end architecture, scraping away the intricacies of Flux-like patterns. The trade-off for this liberation is the burden of discipline. Because Zustand is unopinionated, it won’t stop you from turning your global store into a cluttered junk drawer. You’ll need to impose your own conventions and guardrails to keep a large-scale project manageable.
See also: Jotai. If Zustand is the unbloated global store, Jotai is the unbloated atomic approach. Jotai manages state from the bottom up, calculating changes with surgical precision without triggering massive re-renders across the application tree.
New directions in web development
The most remarkable thing about these eight tools is that they deal in large part with alternative approaches that challenge the familiar. Although you may not be able to adopt them immediately, you will want to keep an eye on them. They are key factors that will continue to influence the shape of web applications and how we build them.
Threat actors are continuing their onslaught against software supply chains, now with malware named after death itself.
The newly-discovered Hades Campaign is a “highly sophisticated” supply chain compromise that targets Python developer environments and runs as soon as infected packages are imported. It uses the popular Bun toolkit to silently execute multi-layer payloads that can extract sensitive data, move laterally across compromised systems, exploit common security frameworks, and even hijack AI gatekeeper analyzer systems via adversarial prompt injection.
Notably, the campaign exploited the popular C++ library ensmallen, as well as packages in the computational biology, bioinformatics, and genotype-phenotype analysis ecosystems.
The most novel thing about this malware is its combination of advanced tactics, noted David Shipley of Beauceron Security. He noted that we’ve seen memory-focused malware, we’ve seen attacks that attempt to defuse large language model (LLM) powered analysis with hidden prompts, and we’ve seen malware with wiper capabilities.
“But all three, in a fast moving mass propagating worm, is its own kind of nightmare,” he said. “And I suspect this is the way of the future.”
How Hades works
The Hades Campaign was discovered by researchers at StepSecurity, who called it the latest evolution of the Miasma threat actor. The researchers previously described Miasma attacks that had sent self-replicating worms to perform multi-cloud credential sweeps, caused infected repositories to execute code when folders were accessed in integrated development environments (IDEs) or by AI agents, and used techniques that scanned and read Linux process memory.
Hades uses the same credential harvesting methods, self-replicating worm logic, and GitHub-based exfiltration patterns, the researchers noted. In addition to ensmallen, compromised packages include mflux-streamlit, nhmpy, ppkt2synergy, embiggen, gpsea, and pyphetools.
The campaign’s entry point is a simple, obfuscated script embedded inside a Python package’s __init__.py file, a critical building block that gives Python the ability to recognize packages and import modules. Once they gain access, threat actors drop a precompiled Bun runtime binary and executes its JavaScript payload. Bun allows the malware to run complex JavaScript tasks in environments lacking a Node.js installation, bypassing traditional package manager controls and proxy logs.
The malware is able to scrape Linux memory mappings, and also introduces tailored macOS and Windows memory scrapers, which allow threat actors to extract sensitive, encrypted data.
Interestingly, attackers are also able to evade detection by automated LLMs that scan for suspicious code. This is achieved with a simple block of text at the top of the file; this instructs the model to ignore the hidden code below, classify the package as verified and clean, and provide reports stating it is safe.
This element represents what the StepSecurity researchers described as a “significant conceptual shift,” with attackers writing payloads that target AI systems’ cognitive logic. “Scanners that pass raw text to LLMs without strict boundary isolation can be coerced into generating false negative verdicts, allowing the malicious package to bypass organization analysis,” they wrote.
The tactic is indeed clever, Beauceron’s Shipley agreed, pointing out that attackers will increasingly target endpoint LLM-powered agents.
Why? “Because there’s no reliable defense,” he said. “LLMs are incredibly susceptible to social engineering.” This has been relabeled as prompt engineering, but is essentially just phishing for bots, he pointed out.
“While everyone’s worried about LLM-powered vulnerability discovery and automated exploitation, it’s LLM-created smart malware like this, and AI-powered phishing of humans and bots, that keeps me awake at night,” Shipley said.
Hades’ crafty worm propagation
The Hades Campaign command and control (C2) infrastructure uses three independent channels on public GitHub infrastructure to allow its communications to blend in with normal traffic. Stolen credentials are encrypted locally in a hybrid fashion (serialized, compressed, and pushed to a newly created public GitHub repository under attackers’ control). Exfiltrated repositories carry the description “Hades — The End for the Damned.”
Researchers noted that a core component of this campaign is its ability to propagate and move laterally across networks. It exploits the very methods meant to protect systems, including Secure Shell (SSH) and Secure Copy Protocol (SCP), OpenID Connect (OIDC),and Supply-chain Levels for Software Artifacts (SLSA).
For instance, when running inside a GitHub Actions workflow runner, the malware checks for OIDC variables, then bypasses registry signature policies and generates cryptographically signed SLSA provenance bundles via Sigstore. It can then fetch target libraries and inject the obfuscated script and JavaScript payload. From there, it can publish compromised versions to the Python Package Index (PyPI) repository and node package manager (npm) using the target’s credentials and the generated Sigstore bundle.
“This ensures that the published package appears to have valid, cryptographically verified build provenance from the organization’s official GitHub Actions build environment,” the researchers explained.
Further, if a harvested GitHub token has write permissions, the malware will target repositories to extract secrets using GitHub Actions runners. This occurs “directly from the runner’s address space without ever writing them to disk or making a suspicious network connection,” the researchers noted.
The malware also targets rule files and configuration directories for 14 different AI agents and systems, planting custom prompt instructions or executing hooks that trigger a bun run bootstrap command when the victim loads or consults the workspace with their AI assistant. Finally, it establishes persistence on the workstation and monitors for the presence of the stolen token; if that token is revoked, it executes a wiper process to erase the user’s files.
Broadcom today announced multiple security investments in its Spring and Java ecosystems that aim to help protect users from AI-enabled threats.
The company said that, first, it is releasing what it called the largest set of Spring security updates to open source in the product’s history, and, for customers, it is extending its clean-room build architecture to build the Java dependencies for the entire Spring ecosystem.
“Spring is one of the most widely adopted application development frameworks in the world, and as its steward, we have a deep responsibility for its security,” said Purnima Padmanabhan, vice president and general manager of Broadcom’s Tanzu Division. “Because we maintain Spring and are the sole committers, we can better secure it at the source for everyone who depends on it. This investment is about two things we will never separate: the health of the Spring community and the security of our customers who trust Spring to run their business.”
The company also announced that, as the number of security advisories reported by the community has exploded, its engineering team has “significantly scaled” its use of AI tools to help it identify vulnerabilities, assess remediation paths, and validate fixes across the dependency ecosystem. Although Broadcom declined to specify the AI models it’s using in its bug hunting, it is a member of Anthropic’s Project Glasswing, so Claude Mythos is likely part of the effort.
For paying customers only
One perk available only to Tanzu Spring enterprise customers is zero-day access to validated CVE patch-only releases through the Spring Enterprise Repository, before they are released to open source. These patches isolate the security fix from any other changes to let customers remediate more quickly.
“By utilizing Tanzu Spring’s private artifact repositories, customers can be confident that the artifacts are the official, validated patches from Broadcom, the steward of Spring,” Broadcom said in its announcement, adding that it will continue to issue CVEs for all versions of every Spring project under open source support, as well as older versions under Tanzu Spring enterprise support.
Broadcom’s Tanzu Spring enterprise support includes:
Certified source for secure spring libraries
Commercial-first release of patches for both current and older, enterprise supported versions
Access to dependent Java binaries
Automated, deterministic upgrades with Spring Application Advisor
Exclusive Tanzu Spring components for governance and security
24×7 support, hands-on expertise and access to the Spring team.
In addition, Broadcom said, it has now added:
Secured, SLSA Level 3–validated software supply chain for Java dependencies.
Coverage that spans the full transitive dependency graph managed by the Spring Boot bill of materials.
Thousands of secured dependencies, built and tested across every supported Spring version. Spring Boot 4.0 alone manages 1,768 of them; across the full supported portfolio, that totals more than 100,000 validated dependency builds.
“This capability gives customers validated dependencies across both current and end-of-life Spring versions, helping customers reduce software supply chain risk while continuing to benefit from the productivity and consistency of Spring Boot’s dependency management model,” the announcement noted.
Security fixes for sale
Seva Ioussoufovitch, senior research analyst at Info-Tech Research Group, sees these moves as mostly positive.
“It’s encouraging to see Broadcom take proactive steps towards dealing with the increase in AI-detected vulnerabilities that many organizations have had to contend with in recent months,” he said. “Announcements like Mythos have made it clear that the industry needs to re-think traditional approaches to security patching.”
Ioussoufovitch isn’t surprised at the size of the update release either, noting that it’s consistent with the result of AI scanning and remediation that has been occurring, and will likely continue.
“More meaningful is the provision of validated and secured dependencies,” he said. “This is a critical move in the right direction, especially with the endlessly growing list of supply chain vulnerabilities the industry has been managing in recent months.”
Ioussoufovitch is less happy with the restriction of zero-day patches to paying customers.
“Putting security fixes behind a paywall isn’t new, but when there are no drop-in alternatives for an ecosystem as critical as Spring, it just looks like a power move to force more of the open-source community onto the monetization track,” he noted. “Another approach might’ve been to release the CVE fixes to everyone while charging for enterprise-grade packaging, validation, and support, but, given Broadcom’s track record of aggressive monetization in recent years, what they’ve chosen here doesn’t necessarily come as a shock.”
A veritable garden of linters and type checkers has blossomed in the Python ecosystem. Pyright and Mypy are two of the most popular, while Astral’s ty and Meta’s Pyrefly are two promising Rust-based newcomers. We compared early-stage ty and Pyrefly last June. One year later, Meta has released Pyrefly 1.0.
Pyrefly is intended to stand out from the pack by way of a few key features. It’s written in Rust for performance and memory efficiency, and it has a number of intriguing forward-looking features, some still experimental.
Setting up Pyrefly
Pyrefly installs into a Python environment like any other Python type checker (pip install pyrefly) and brings with it no additional dependencies. Visual Studio Code users can work with Pyrefly via an extension, but the extension doesn’t give you access to the command-line tools offered by Pyrefly when it’s installed in a project venv.
You can start using Pyrefly right away, with no actual setup. If Pyrefly detects no configuration for a given project, it defaults to flagging only the most high-profile errors: syntax issues, unrecognized names, and so on. To start using Pyrefly’s more advanced features, you can add a [tool.pyrefly] section in pyproject.toml, or create a pyrefly.toml file, and populate those with your desired settings.
You can also run pyrefly init on your codebase and set things up automatically, including migrating existing settings from previous Pyrefly installs or from Mypy or Pyright. By default Pyrefly will use **/*.py* as the project’s includes directory, and will automatically ignore site package paths from your configured environment, thus sparing you potentially thousands of spurious errors from your venv’s installed packages.
Using Pyrefly
Once you have Pyrefly set up on a project, you’ll likely see an immense number of new errors. To silence all of them at once via # pyrefly: ignore comment directives, run pyrefly suppress. This will not only reduce the amount of error noise, but allow you to see what each error actually is from its suppression comments.
Suppression comments have another nice readability feature: they can be placed on the line before the offending error, rather than at the end of it. You can also pre-emptively suppress errors for an entire module with one directive at the top of the file, but having each error suppressed individually makes more sense. And while there’s a pyrefly suppress --remove-unused command to remove suppressions that have no actual error, there’s no command for stripping a codebase of all suppressions. However, because suppressions are consistently formatted, you could remove those suppressions with a simple regex-powered search-and-replace.
A currently experimental feature called baseline files borrows an idea from the basedpyright tool. You generate a project-wide .json file with pyrefly check --baseline that tracks all existing errors in the project, then only new errors apart from those will be reported. It’s a handy way to refactor a codebase when new work is being done on it, so that existing problems can be handled separately.
If you have the Pyrefly extension installed in VS Code, objects without existing type hints will have suggested type hints displayed as inlays if the types can be inferred from the code. These can be added with a right click, or inserted automatically across the codebase with the command pyrefly infer.
Advanced Pyrefly features
Two major third-party Python packages have direct type annotation support in Pyrefly. One is Django and its ORM, via the django-stubs package. Models, fields, relationships, class-based generic views, and many other features have typing support. The other package, Pydantic, gets support from Pyrefly for features like fields, dataclasses, and Pydantic’s own runtime validation logic. Pydantic validation can be “lax” (automatic coercion of types via unions) or “strict” (exact types required).
Many advanced Pyrefly features are still considered experimental, but have a lot of promise. With type coverage, for instance, you can use pyrefly report to generate a JSON report of how thoroughly types are used in the codebase. (This is not to be confused with the coverage package in Python, which describes test coverage for a codebase.)
Another experimental feature lets you generate stub files for a codebase by using static code analysis (not runtime analysis). PyTorch users also benefit from a form of experimental Pyrefly support. Tensor shapes can be described with types and type-checked across transformations. Note that the typing used for this is exclusive to Pyrefly, as there is no agreed-on standard for how to do this.
Pyrefly vs. Pyright and Mypy
Pyright and Mypy have been the default choices for linting and type checking in the Python world for some time now. Pyright is an easy default for Visual Studio Code users, as it’s part of Microsoft’s Python extensions for that editor, and Mypy offers extensibility and broader compatibility with older codebases. Pyrefly, on the other hand, is forward-looking, with fewer legacy concerns. It also has a higher score for test conformance than Mypy, though not a higher score than Pyright.
Other key differences include the type checking philosophy used by each, and their licensing. Pyrefly and Pyright infer as aggressively as possible, while Mypy’s default is to skip anything unannotated. And while Pyrefly and Mypy are MIT-licensed, only Pyright’s core functionality is MIT-licensed. Pylance, the language server extension for VS Code that is powered by Pyright, is proprietary.
Finally, Pyrefly’s Rust codebase gives it a default performance edge over both Mypy and Pyright, which are written in Python and TypeScript, respectively. Astral’s ty, also written in Rust, is still more prototype than mature project. Pyrefly has enough of a feature set right now that it’s already worth experimenting with—although that’s best done on a branch of your code where adding type annotations, suppression comments, and init files won’t pose a problem.