Plugin how-to guides

This page provides task-oriented guides for creating each type of virtualenv plugin.

Create a discovery plugin

Discovery plugins locate Python interpreters. Register your plugin under the virtualenv.discovery entry point group.

Implement the Discover interface:

from __future__ import annotations

from argparse import ArgumentParser

from virtualenv.config.cli.parser import VirtualEnvOptions
from virtualenv.discovery.discover import Discover
from virtualenv.discovery.py_info import PythonInfo


class CustomDiscovery(Discover):
    @classmethod
    def add_parser_arguments(cls, parser: ArgumentParser) -> None:
        parser.add_argument("--custom-opt", help="custom discovery option")

    def __init__(self, options: VirtualEnvOptions) -> None:
        super().__init__(options)
        self.custom_opt = options.custom_opt

    def run(self) -> PythonInfo | None:
        # Locate Python interpreter and return PythonInfo
        return PythonInfo.from_exe(str(self._find_python()))

    def _find_python(self) -> str:
        # Implementation-specific logic
        ...

Register the entry point:

[virtualenv.discovery]
custom = your_package.discovery:CustomDiscovery

Create a creator plugin

Creator plugins build the virtual environment structure. Register under virtualenv.create.

Implement the Creator interface:

from __future__ import annotations

from argparse import ArgumentParser

from virtualenv.app_data.base import AppData
from virtualenv.config.cli.parser import VirtualEnvOptions
from virtualenv.create.creator import Creator, CreatorMeta
from virtualenv.discovery.py_info import PythonInfo


class CustomCreator(Creator):
    @classmethod
    def add_parser_arguments(
        cls,
        parser: ArgumentParser,
        interpreter: PythonInfo,
        meta: CreatorMeta,
        app_data: AppData,
    ) -> None:
        parser.add_argument("--custom-creator-opt", help="custom creator option")

    def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None:
        super().__init__(options, interpreter)
        self.custom_opt = options.custom_creator_opt

    def create(self) -> None:
        # Create directory structure
        self.bin_dir.mkdir(parents=True, exist_ok=True)
        # Copy or symlink Python executable
        self.install_python()
        # Set up site-packages
        self.install_site_packages()
        # Write pyvenv.cfg
        self.set_pyenv_cfg()

Register the entry point using a naming pattern that matches platform and Python version:

[virtualenv.create]
cpython3-posix = virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Posix
cpython3-win = virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Windows

Create a seeder plugin

Seeder plugins install initial packages into the virtual environment. Register under virtualenv.seed.

Override cannot_seed to reject target interpreters the seeder does not support. The base returns None for every interpreter; return a message instead and selection rejects the seeder before creating the environment, surfacing your message to the user. A plugin can therefore serve Python versions the bundled seeders no longer ship wheels for, such as a version past its support window.

Implement the Seeder interface:

from __future__ import annotations

from argparse import ArgumentParser

from virtualenv.app_data.base import AppData
from virtualenv.config.cli.parser import VirtualEnvOptions
from virtualenv.create.creator import Creator
from virtualenv.discovery.py_info import PythonInfo
from virtualenv.seed.seeder import Seeder


class CustomSeeder(Seeder):
    @classmethod
    def add_parser_arguments(
        cls, parser: ArgumentParser, interpreter: PythonInfo, app_data: AppData
    ) -> None:
        parser.add_argument("--custom-seed-opt", help="custom seeder option")

    @classmethod
    def cannot_seed(cls, interpreter: PythonInfo) -> str | None:
        # ship wheels down to Python 3.6, for example
        if interpreter.version_info[:2] >= (3, 6):
            return None
        return "custom seeder ships wheels only for Python 3.6 and later"

    def __init__(self, options: VirtualEnvOptions, enabled: bool) -> None:
        super().__init__(options, enabled)
        self.custom_opt = options.custom_seed_opt

    def run(self, creator: Creator) -> None:
        # Install packages into creator.bin_dir / creator.script("pip")
        self._install_packages(creator)

    def _install_packages(self, creator: Creator) -> None:
        # Implementation-specific logic
        ...

Register the entry point:

[virtualenv.seed]
custom = your_package.seed:CustomSeeder

Create an activator plugin

Activator plugins generate shell activation scripts. Register under virtualenv.activate.

Implement the Activator interface:

from __future__ import annotations

from pathlib import Path

from virtualenv.activation.activator import Activator
from virtualenv.create.creator import Creator


class CustomShellActivator(Activator):
    def generate(self, creator: Creator) -> list[Path]:
        # Generate activation script content
        script_content = self._render_template(creator)
        # Write to activation directory
        dest = creator.bin_dir / self.script_name
        dest.write_text(script_content)
        return [dest]

    def _render_template(self, creator: Creator) -> str:
        # Return activation script content
        return f"""
        # Custom shell activation script
        export VIRTUAL_ENV="{creator.dest}"
        export PATH="{creator.bin_dir}:$PATH"
        """

    @property
    def script_name(self) -> str:
        return "activate.custom"

Register the entry point:

[virtualenv.activate]
bash = virtualenv.activation.bash:BashActivator
fish = virtualenv.activation.fish:FishActivator
custom = your_package.activation:CustomShellActivator

Package and distribute a plugin

Use pyproject.toml to declare entry points:

[project]
name = "virtualenv-custom-plugin"
version = "1.0.0"
dependencies = ["virtualenv>=20.0.0"]

[project.entry-points."virtualenv.discovery"]
custom = "virtualenv_custom.discovery:CustomDiscovery"

[project.entry-points."virtualenv.create"]
custom-posix = "virtualenv_custom.creator:CustomCreator"

[project.entry-points."virtualenv.seed"]
custom = "virtualenv_custom.seeder:CustomSeeder"

[project.entry-points."virtualenv.activate"]
custom = "virtualenv_custom.activator:CustomActivator"

[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

Install your plugin alongside virtualenv:

$ pip install virtualenv-custom-plugin

Or in development mode:

$ pip install -e /path/to/virtualenv-custom-plugin

Test your plugin by creating a virtual environment:

$ virtualenv --discovery=custom --creator=custom-posix --seeder=custom --activators=custom test-env