Python venv and virtualenv Practice Problems & Exercises
Practice: venv and virtualenv
← Back to lessonEasy
Write a function that detects whether the current Python process is running inside a virtual environment. Use two independent checks: comparing sys.prefix to sys.base_prefix, and inspecting the VIRTUAL_ENV environment variable.
import sys
import os
def is_venv_active():
prefix_check = sys.prefix != sys.base_prefix
env_check = os.environ.get("VIRTUAL_ENV") is not None
return prefix_check or env_check
print(is_venv_active())Solution
import sys
import os
def is_venv_active():
prefix_check = sys.prefix != sys.base_prefix
env_check = os.environ.get("VIRTUAL_ENV") is not None
return prefix_check or env_check
sys.base_prefix is the canonical check. It was added in Python 3.3 specifically for this purpose and always points to the system Python installation, even when inside a venv. sys.prefix is redirected to the venv directory when a venv is active. The VIRTUAL_ENV variable is set by the activate shell script — it is not present when you use python -m venv and run the interpreter directly without activating.
import sys
import os
def is_venv_active():
"""Return True if the current Python process is running
inside a virtual environment, False otherwise.
Check both sys.prefix != sys.base_prefix and the
VIRTUAL_ENV environment variable.
"""
# TODO: implement
passExpected Output
TrueHints
Hint 1: In a venv, sys.prefix points to the venv directory while sys.base_prefix points to the system Python.
Hint 2: Also check os.environ.get("VIRTUAL_ENV") — this is set by the activate script.
Parse a pyvenv.cfg file from its string content. This file lives at the root of every venv and records where the base Python lives, whether system site-packages are included, and which Python version was used.
def parse_pyvenv_cfg(content):
result = {}
for line in content.splitlines():
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, _, value = line.partition('=')
result[key.strip()] = value.strip()
return result
sample = """home = /usr/bin
include-system-site-packages = false
version = 3.11.5"""
print(parse_pyvenv_cfg(sample))Solution
def parse_pyvenv_cfg(content):
result = {}
for line in content.splitlines():
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, _, value = line.partition('=')
result[key.strip()] = value.strip()
return result
str.partition('=') splits on the first = only, which is correct because values could theoretically contain = signs. The pyvenv.cfg file is CPython's record of the venv's parentage — the home field is what Python uses to compute sys.base_prefix at startup. When you run python inside a venv, the interpreter walks up from the executable location to find this file and configure itself.
def parse_pyvenv_cfg(content):
"""Parse the contents of a pyvenv.cfg file.
Each non-blank, non-comment line has the form:
key = value
Return a dict of {key: value} with leading/trailing
whitespace stripped from both key and value.
Example input:
home = /usr/bin
include-system-site-packages = false
version = 3.11.5
"""
# TODO: implement
passExpected Output
{'home': '/usr/bin', 'include-system-site-packages': 'false', 'version': '3.11.5'}Hints
Hint 1: Split each line on "=" but only on the first "=" to handle values that may contain "=".
Hint 2: Skip blank lines and lines starting with "#".
Write a classifier that inspects a Python executable path and categorizes it as coming from a plain venv, pyenv, conda, or the system. Use string matching only — no file I/O.
import os
def classify_python_path(executable_path):
p = executable_path
if '.pyenv' in p:
return 'pyenv'
if 'conda' in p or '/envs/' in p:
return 'conda'
if ('venv' in p or '.venv' in p or '/bin/python' in p) and 'base_prefix' not in p:
if '/usr/bin' not in p and '/usr/local/bin' not in p:
return 'venv'
return 'system'
paths = [
'/home/user/projects/myapp/.venv/bin/python3',
'/home/user/.pyenv/versions/3.11.5/bin/python',
'/home/user/miniconda3/envs/ml/bin/python',
'/usr/bin/python3',
]
for p in paths:
print(classify_python_path(p))Solution
def classify_python_path(executable_path):
p = executable_path
if '.pyenv' in p:
return 'pyenv'
if 'conda' in p or '/envs/' in p:
return 'conda'
if 'venv' in p or '.venv' in p:
return 'venv'
return 'system'
Path classification is a common sysadmin and tooling task. Real tools like pip and IDEs use this kind of heuristic to display the active environment. The order of checks matters: check for pyenv first because pyenv paths never contain "venv" or "conda", then conda (it uses /envs/), then generic venv patterns. sys.executable in a real program gives the full path to the running Python binary — this is more reliable than which python because it reflects the actual interpreter regardless of PATH manipulation.
import os
def classify_python_path(executable_path):
"""Given a Python executable path string, classify it.
Return one of:
- 'venv' if path contains '/bin/python' inside a venv-like dir
- 'pyenv' if path contains '.pyenv'
- 'conda' if path contains 'conda' or 'envs'
- 'system' otherwise
Use simple string inspection — no filesystem access.
"""
# TODO: implement
passExpected Output
venv
pyenv
conda
systemHints
Hint 1: Check for substrings: ".pyenv" for pyenv, "conda" or "/envs/" for conda, "/lib/python" combined with "venv" for venv.
Hint 2: Order your checks carefully — a conda env path might also contain "lib/python".
Model the key variable assignments that a venv activate script performs. This helps understand why activating a venv changes your shell environment — it mutates PATH and VIRTUAL_ENV, and modifies PS1 to show the env name in the prompt.
import os
def generate_activate_fragment(venv_path):
venv_name = os.path.basename(venv_path)
return {
'VIRTUAL_ENV': venv_path,
'PATH': venv_path + '/bin:$PATH',
'PS1': '(' + venv_name + ') $PS1',
}
print(generate_activate_fragment('/home/user/projects/.venv'))Solution
import os
def generate_activate_fragment(venv_path):
venv_name = os.path.basename(venv_path)
return {
'VIRTUAL_ENV': venv_path,
'PATH': venv_path + '/bin:$PATH',
'PS1': '(' + venv_name + ') $PS1',
}
Activation is purely a PATH manipulation. The activate script prepends venv/bin to PATH so that python and pip resolve to the venv executables instead of the system ones. It also saves and later restores the old PATH and PS1 in _OLD_VIRTUAL_PATH and _OLD_VIRTUAL_PS1 so deactivate can undo the changes. This means activation is entirely optional — you can always invoke the venv's Python directly via ./venv/bin/python without ever sourcing the activate script.
def generate_activate_fragment(venv_path):
"""Return the key shell variable assignments from
a venv activate script for a given venv_path.
Return a dict with keys:
- 'VIRTUAL_ENV': the venv_path itself
- 'PATH': venv_path + '/bin' prepended to a placeholder '$PATH'
- 'PS1': '(venv_name) ' prepended to '$PS1'
venv_name is the last component of venv_path.
"""
# TODO: implement
passExpected Output
{'VIRTUAL_ENV': '/home/user/projects/.venv', 'PATH': '/home/user/projects/.venv/bin:$PATH', 'PS1': '(.venv) $PS1'}Hints
Hint 1: Use os.path.basename(venv_path) to extract the venv name.
Hint 2: PATH is venv_path + "/bin" + ":" + "$PATH".
Medium
Write two utility functions: one that extracts all site-packages directories from sys.path, and one that counts installed packages in a given site-packages directory by counting .dist-info entries.
import sys
import os
def get_site_packages_dirs():
return sorted(p for p in sys.path if 'site-packages' in p)
def count_installed_packages(site_packages_dir):
if not os.path.isdir(site_packages_dir):
return 0
return sum(
1 for entry in os.listdir(site_packages_dir)
if entry.endswith('.dist-info')
)
dirs = get_site_packages_dirs()
print("site_packages found:", len(dirs) >= 0)
if dirs:
count = count_installed_packages(dirs[0])
print("package count >= 0:", count >= 0)
else:
print("package count >= 0: True")Solution
import sys
import os
def get_site_packages_dirs():
return sorted(p for p in sys.path if 'site-packages' in p)
def count_installed_packages(site_packages_dir):
if not os.path.isdir(site_packages_dir):
return 0
return sum(
1 for entry in os.listdir(site_packages_dir)
if entry.endswith('.dist-info')
)
Each installed package leaves a .dist-info directory containing metadata like METADATA, WHEEL, and RECORD. This is the canonical way pip tracks what is installed — pip list reads these directories rather than scanning Python files. A fresh venv typically has just pip and setuptools (2-3 .dist-info dirs). This approach is the basis of tools like pip list --format=json and importlib.metadata.
import sys
def get_site_packages_dirs():
"""Return a list of all site-packages directories
currently on sys.path.
Filter for paths that contain 'site-packages'.
Sort the result alphabetically.
"""
# TODO: implement
pass
def count_installed_packages(site_packages_dir):
"""Count the number of installed packages in a
site-packages directory by counting .dist-info dirs.
Return 0 if the directory does not exist.
"""
import os
# TODO: implement
passExpected Output
site_packages found: True
package count >= 0: TrueHints
Hint 1: Filter sys.path for entries containing the string "site-packages".
Hint 2: Count directories ending in ".dist-info" — each represents one installed package.
Virtual environments have different internal layouts on Windows vs Unix. Write a function that returns the correct Python executable path for a given venv directory, supporting both platforms.
This matters when writing tooling that manipulates venvs programmatically (CI scripts, IDE plugins, build tools).
import os
def get_venv_python(venv_dir, platform=None):
if platform is None:
platform = 'win32' if os.name == 'nt' else 'unix'
if platform == 'win32':
return os.path.join(venv_dir, 'Scripts', 'python.exe')
return os.path.join(venv_dir, 'bin', 'python3')
print(get_venv_python('/home/user/.venv', platform='unix'))
print(get_venv_python('C:/projects/.venv', platform='win32'))Solution
import os
def get_venv_python(venv_dir, platform=None):
if platform is None:
platform = 'win32' if os.name == 'nt' else 'unix'
if platform == 'win32':
return os.path.join(venv_dir, 'Scripts', 'python.exe')
return os.path.join(venv_dir, 'bin', 'python3')
The Windows venv layout uses Scripts\ instead of bin\ — a consequence of Windows not having a POSIX /usr/bin convention. This difference breaks many shell scripts that hardcode venv/bin/python. Robust tooling always uses os.path.join with platform detection or uses sysconfig.get_path('scripts') to get the correct scripts directory for the current platform. The virtualenv package adds a compatibility layer that creates both layouts on Windows.
import os
def get_venv_python(venv_dir, platform=None):
"""Return the path to the Python executable inside
a venv directory for the given platform.
platform: 'win32', 'linux', or 'darwin'
If platform is None, use os.name to detect.
Windows layout: venv_dir/Scripts/python.exe
Unix layout: venv_dir/bin/python3
"""
# TODO: implement
passExpected Output
/home/user/.venv/bin/python3
C:/projects/.venv/Scripts/python.exeHints
Hint 1: On Windows (os.name == "nt"), the layout is Scripts/ and the binary is python.exe.
Hint 2: On Unix systems, the layout is bin/ and the binary is python3.
Write a function that checks whether a package is available in the current environment and whether its location is inside a venv. This is useful for pre-flight checks in scripts that depend on optional packages.
import sys
import importlib.util
def check_package_available(package_name):
spec = importlib.util.find_spec(package_name)
if spec is None:
return {'available': False, 'location': None, 'in_venv': False}
location = spec.origin or ''
in_venv = (
'site-packages' in location and
sys.prefix != sys.base_prefix
)
return {
'available': True,
'location': location,
'in_venv': in_venv,
}
result = check_package_available('os')
print(result['available'], result['location'] is not None)Solution
import sys
import importlib.util
def check_package_available(package_name):
spec = importlib.util.find_spec(package_name)
if spec is None:
return {'available': False, 'location': None, 'in_venv': False}
location = spec.origin or ''
in_venv = (
'site-packages' in location and
sys.prefix != sys.base_prefix
)
return {
'available': True,
'location': location,
'in_venv': in_venv,
}
importlib.util.find_spec is the correct way to check package availability without actually importing the package (which can have side effects). spec.origin gives the path to the package's __init__.py or the .py file for single-module packages. Standard library modules will have a location inside the Python installation, not site-packages. This pattern is used by tools like pytest to detect optional plugins at startup.
import sys
import importlib.util
def check_package_available(package_name):
"""Check whether a package is importable in the
current Python environment.
Return a dict with:
- 'available': bool
- 'location': str path to the package or None
- 'in_venv': bool (True if location is inside a venv)
"""
# TODO: implement
passExpected Output
{'available': True, 'location': '...', 'in_venv': True or False}Hints
Hint 1: Use importlib.util.find_spec(package_name) — it returns None if the package is not importable.
Hint 2: Check if the spec.origin path contains "site-packages" to determine if it is installed (vs stdlib).
Model how Python constructs sys.path inside a venv. When include-system-site-packages is false (the default), only the venv's own site-packages is on the path. When true, system paths are also included — useful for environments that need system-level packages like apt-installed libraries.
import os
def build_venv_sys_path(venv_dir, python_version, include_system=False):
paths = [
os.path.join(venv_dir, 'lib', 'python' + python_version, 'site-packages')
]
if include_system:
paths.extend([
'/usr/lib/python' + python_version,
'/usr/lib/python' + python_version + '/lib-dynload',
])
return paths
print(build_venv_sys_path('/home/user/.venv', '3.11'))
print(build_venv_sys_path('/home/user/.venv', '3.11', include_system=True))Solution
import os
def build_venv_sys_path(venv_dir, python_version, include_system=False):
paths = [
os.path.join(venv_dir, 'lib', 'python' + python_version, 'site-packages')
]
if include_system:
paths.extend([
'/usr/lib/python' + python_version,
'/usr/lib/python' + python_version + '/lib-dynload',
])
return paths
The include-system-site-packages flag in pyvenv.cfg controls whether system paths are appended. The default is false, which gives full isolation — nothing from the system Python leaks in. Setting it to true is useful in Docker images where some heavy packages (numpy, scipy, GDAL) are installed via the OS package manager and you do not want to reinstall them in every venv. You can set this at creation time with python -m venv --system-site-packages myenv.
import os
def build_venv_sys_path(venv_dir, python_version, include_system=False):
"""Build the expected sys.path entries for a venv.
Always include:
- venv_dir/lib/pythonX.Y/site-packages
If include_system is True, also include:
- /usr/lib/pythonX.Y
- /usr/lib/pythonX.Y/lib-dynload
python_version: string like '3.11'
Return a list of path strings.
"""
# TODO: implement
passExpected Output
['/home/user/.venv/lib/python3.11/site-packages']Hints
Hint 1: Format the lib directory as: venv_dir + "/lib/python" + python_version + "/site-packages"
Hint 2: The include_system flag corresponds to include-system-site-packages = true in pyvenv.cfg.
Hard
Write a staleness checker for virtual environments. A venv is considered stale when the Python version it was created with differs in major or minor version from the currently active Python. Patch version differences are acceptable.
This is the kind of check that IDE integrations and CI systems perform before using a cached venv.
import os
def check_venv_staleness(venv_dir, current_python_version):
cfg_path = os.path.join(venv_dir, 'pyvenv.cfg')
if not os.path.exists(cfg_path):
return {
'stale': True,
'venv_version': None,
'current_version': current_python_version,
'reason': 'pyvenv.cfg not found',
}
venv_version = None
try:
with open(cfg_path) as f:
for line in f:
line = line.strip()
if line.startswith('version'):
_, _, val = line.partition('=')
venv_version = val.strip()
break
except OSError:
return {
'stale': True,
'venv_version': None,
'current_version': current_python_version,
'reason': 'could not read pyvenv.cfg',
}
if venv_version is None:
return {
'stale': True,
'venv_version': None,
'current_version': current_python_version,
'reason': 'version field missing from pyvenv.cfg',
}
def major_minor(v):
parts = v.split('.')
return (parts[0], parts[1]) if len(parts) >= 2 else (v, '')
if major_minor(venv_version) != major_minor(current_python_version):
return {
'stale': True,
'venv_version': venv_version,
'current_version': current_python_version,
'reason': 'version mismatch: venv=' + venv_version + ', current=' + current_python_version,
}
return {
'stale': False,
'venv_version': venv_version,
'current_version': current_python_version,
'reason': 'versions compatible',
}
# Simulate a venv with wrong version
import tempfile, os
with tempfile.TemporaryDirectory() as tmpdir:
cfg = os.path.join(tmpdir, 'pyvenv.cfg')
with open(cfg, 'w') as f:
f.write('home = /usr/bin\nversion = 3.10.4\n')
result = check_venv_staleness(tmpdir, '3.11.5')
print(result)Solution
import os
def check_venv_staleness(venv_dir, current_python_version):
cfg_path = os.path.join(venv_dir, 'pyvenv.cfg')
if not os.path.exists(cfg_path):
return {'stale': True, 'venv_version': None,
'current_version': current_python_version,
'reason': 'pyvenv.cfg not found'}
venv_version = None
with open(cfg_path) as f:
for line in f:
if line.strip().startswith('version'):
_, _, val = line.partition('=')
venv_version = val.strip()
break
if venv_version is None:
return {'stale': True, 'venv_version': None,
'current_version': current_python_version,
'reason': 'version field missing from pyvenv.cfg'}
def mm(v):
p = v.split('.')
return (p[0], p[1]) if len(p) >= 2 else (v, '')
if mm(venv_version) != mm(current_python_version):
return {'stale': True, 'venv_version': venv_version,
'current_version': current_python_version,
'reason': 'version mismatch: venv=' + venv_version + ', current=' + current_python_version}
return {'stale': False, 'venv_version': venv_version,
'current_version': current_python_version,
'reason': 'versions compatible'}
Stale venvs are a silent CI failure mode. When a Python minor version upgrade happens (3.10 to 3.11), the ABI changes — C extension modules compiled for 3.10 will crash when loaded by 3.11. Patch versions (3.11.4 to 3.11.5) are ABI-compatible. A robust CI pipeline checks pyvenv.cfg before using a cached venv and recreates it on major.minor mismatch. Tools like tox and nox do this automatically.
import os
def check_venv_staleness(venv_dir, current_python_version):
"""Check if a venv is stale (created with a different
Python version than is currently active).
Read venv_dir/pyvenv.cfg, parse the 'version' field.
Compare to current_python_version.
Return a dict:
- 'stale': bool
- 'venv_version': str (from cfg) or None
- 'current_version': str (passed in)
- 'reason': descriptive string
If pyvenv.cfg does not exist, return stale=True with
reason='pyvenv.cfg not found'.
"""
# TODO: implement
passExpected Output
{'stale': True, 'venv_version': '3.10.4', 'current_version': '3.11.5', 'reason': 'version mismatch: venv=3.10.4, current=3.11.5'}Hints
Hint 1: Parse pyvenv.cfg line by line, splitting on "=" to get key-value pairs.
Hint 2: Compare the major.minor components — patch version mismatch is acceptable, major.minor mismatch is stale.
Write a venv discovery tool that walks a directory tree and finds all virtual environments. This is the kind of utility that IDE workspace scanners, direnv plugins, and project management tools use to auto-detect environments.
Critically, do not recurse into a detected venv — venvs contain their own lib/python3.x/ directories which would produce false positives.
import os
def find_venvs(root_dir, max_depth=3):
venvs = []
root_dir = os.path.abspath(root_dir)
def is_venv(path):
if os.path.exists(os.path.join(path, 'pyvenv.cfg')):
return True
if os.path.exists(os.path.join(path, 'bin', 'python3')):
return True
if os.path.exists(os.path.join(path, 'Scripts', 'python.exe')):
return True
return False
for dirpath, dirnames, filenames in os.walk(root_dir, topdown=True):
rel = os.path.relpath(dirpath, root_dir)
depth = 0 if rel == '.' else rel.count(os.sep) + 1
if depth >= max_depth:
dirnames.clear()
continue
to_remove = []
for d in dirnames:
full = os.path.join(dirpath, d)
if is_venv(full):
venvs.append(full)
to_remove.append(d)
for d in to_remove:
dirnames.remove(d)
return sorted(venvs)
import tempfile
with tempfile.TemporaryDirectory() as tmp:
os.makedirs(os.path.join(tmp, 'project1', '.venv', 'bin'), exist_ok=True)
open(os.path.join(tmp, 'project1', '.venv', 'pyvenv.cfg'), 'w').close()
os.makedirs(os.path.join(tmp, 'project2', 'env'), exist_ok=True)
open(os.path.join(tmp, 'project2', 'env', 'pyvenv.cfg'), 'w').close()
found = find_venvs(tmp)
print("Found", len(found), "venvs")Solution
import os
def find_venvs(root_dir, max_depth=3):
venvs = []
root_dir = os.path.abspath(root_dir)
def is_venv(path):
return (
os.path.exists(os.path.join(path, 'pyvenv.cfg')) or
os.path.exists(os.path.join(path, 'bin', 'python3')) or
os.path.exists(os.path.join(path, 'Scripts', 'python.exe'))
)
for dirpath, dirnames, filenames in os.walk(root_dir, topdown=True):
rel = os.path.relpath(dirpath, root_dir)
depth = 0 if rel == '.' else rel.count(os.sep) + 1
if depth >= max_depth:
dirnames.clear()
continue
to_remove = []
for d in list(dirnames):
full = os.path.join(dirpath, d)
if is_venv(full):
venvs.append(full)
to_remove.append(d)
for d in to_remove:
dirnames.remove(d)
return sorted(venvs)
The key trick is mutating dirnames in-place during os.walk with topdown=True. When you remove an entry from dirnames, os.walk skips descending into it. This prevents the scanner from walking into venv/lib/python3.11/site-packages/numpy/... — which contains thousands of files and would make the scan extremely slow. This pattern (walk + prune) is used by tools like fd, ripgrep, and VS Code's file watcher.
import os
def find_venvs(root_dir, max_depth=3):
"""Walk the directory tree under root_dir (up to max_depth)
and find all virtual environments.
A directory is a venv if it contains:
- pyvenv.cfg at its root, OR
- bin/python3 (Unix) or Scripts/python.exe (Windows)
Return a list of absolute paths to venv roots.
Do NOT recurse into venv directories themselves.
"""
# TODO: implement
passExpected Output
['/projects/api/.venv', '/projects/web/.venv']Hints
Hint 1: Use os.walk with topdown=True. When you find a venv dir, add it to results and remove it from dirs to prevent recursion into it.
Hint 2: Track current depth by counting os.sep in the relative path from root_dir.
Generate a complete environment reproducibility snapshot. This is the kind of report that data science notebooks and ML experiment trackers attach to run logs so that results can be reproduced later — knowing exact package versions is critical when a model trained on numpy 1.24 behaves differently on numpy 2.0.
import sys
import importlib.metadata
def generate_env_report():
vi = sys.version_info
python_version = str(vi.major) + '.' + str(vi.minor) + '.' + str(vi.micro)
packages = []
for dist in importlib.metadata.distributions():
name = dist.metadata.get('Name', '')
version = dist.metadata.get('Version', '')
if name:
packages.append(name.lower() + '==' + version)
packages = sorted(set(packages))
return {
'python_version': python_version,
'executable': sys.executable,
'is_venv': sys.prefix != sys.base_prefix,
'platform': sys.platform,
'installed_packages': packages,
'package_count': len(packages),
}
report = generate_env_report()
print("python_version set:", bool(report['python_version']))
print("executable set:", bool(report['executable']))
print("is_venv:", report['is_venv'])
print("package_count:", report['package_count'])Solution
import sys
import importlib.metadata
def generate_env_report():
vi = sys.version_info
python_version = str(vi.major) + '.' + str(vi.minor) + '.' + str(vi.micro)
packages = []
for dist in importlib.metadata.distributions():
name = dist.metadata.get('Name', '')
version = dist.metadata.get('Version', '')
if name:
packages.append(name.lower() + '==' + version)
packages = sorted(set(packages))
return {
'python_version': python_version,
'executable': sys.executable,
'is_venv': sys.prefix != sys.base_prefix,
'platform': sys.platform,
'installed_packages': packages,
'package_count': len(packages),
}
importlib.metadata (stdlib since 3.8) is the proper API for reading installed package metadata — more reliable than parsing pip list output or scanning directories manually. importlib.metadata.distributions() yields every installed distribution found on sys.path. The set() call deduplicates because the same distribution can appear multiple times if it is installed in both a venv and system site-packages (when include-system-site-packages = true). This report format is essentially a machine-readable pip freeze.
import sys
import importlib.metadata
def generate_env_report():
"""Generate a reproducibility report for the current
Python environment. Return a dict with:
- 'python_version': sys.version_info as 'X.Y.Z'
- 'executable': sys.executable
- 'is_venv': bool
- 'platform': sys.platform
- 'installed_packages': list of 'name==version' strings,
sorted alphabetically, from importlib.metadata
- 'package_count': int
"""
# TODO: implement
passExpected Output
python_version set, executable set, is_venv bool, packages listHints
Hint 1: Use importlib.metadata.packages_distributions() or importlib.metadata.distributions() to iterate installed packages.
Hint 2: Each distribution has .metadata["Name"] and .version attributes.
