❌

Reading view

10 tips for getting better R code from your AI coding agent

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:

claude mcp add -s "user" r-btw -- Rscript -e "btw::btw_mcp_server(tools = btw::btw_tools('btw_tool_run_r', 'docs', 'env'))"

You only need to do this once.

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.

test_claude_code_btw

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.

claude_plan1


Foundry

claude_plan2 rev

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.

Wiegold’s suggestion? “Install both. Try others. Stay flexible.”

In summary

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.”

  •  

It’s crunch time for Java modernization

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.


  •  

How to use virtual environments in 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:

  1. 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.
  2. 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.
  3. You want to experiment with a specific combination of packages under highly controlled circumstances, for instance to test cross-compatibility or backward compatibility.
  4. 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.

With PyCharm, you can use the IDE’s own package management interface to manage the packages installed in your project.

Managing packages in Python virtual environments

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.

  •  

How to use virtual environments in 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:

  1. 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.
  2. 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.
  3. You want to experiment with a specific combination of packages under highly controlled circumstances, for instance to test cross-compatibility or backward compatibility.
  4. 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.

With PyCharm, you can use the IDE’s own package management interface to manage the packages installed in your project.

Managing packages in Python virtual environments

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.

  •  

8 cutting-edge web development tools you don’t want to miss

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.

  •  

Meet Hades: The malware that lies to AI security agents

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 beefs up Spring security to protect against AI-enabled attacks

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.”

  •  

Pyrefly 1.0: A fast, forward-looking Python linter

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.

  •  
❌