Python Installing Python Properly: Practice Problems & Exercises
Practice: Installing Python Properly
← Back to lessonEasy
Write a function that returns a dictionary containing the Python version components (major, minor, micro) and the full version string. Use only the sys module.
import sys
def get_python_version():
"""Return a dict with major, minor, micro version numbers and the full version string."""
pass
info = get_python_version()
print(f"Python {info['major']}.{info['minor']}.{info['micro']}")
print(f"Full version: {info['full']}")
print(f"Is Python 3.8+: {info['major'] == 3 and info['minor'] >= 8}")Solution
import sys
def get_python_version():
"""Return a dict with major, minor, micro version numbers and the full version string."""
return {
"major": sys.version_info.major,
"minor": sys.version_info.minor,
"micro": sys.version_info.micro,
"full": sys.version,
}
info = get_python_version()
print(f"Python {info['major']}.{info['minor']}.{info['micro']}")
print(f"Full version: {info['full']}")
print(f"Is Python 3.8+: {info['major'] == 3 and info['minor'] >= 8}")Why this matters:
sys.version_info is a named tuple, so you can access components by name (major, minor, micro, releaselevel, serial). This is far more reliable than parsing sys.version as a string. In production scripts, version checks like sys.version_info >= (3, 8) are the standard way to enforce minimum Python versions.
import sys
def get_python_version():
"""Return a dict with major, minor, micro version numbers and the full version string."""
# Your code here
pass
info = get_python_version()
print(f"Python {info['major']}.{info['minor']}.{info['micro']}")
print(f"Full version: {info['full']}")
print(f"Is Python 3.8+: {info['major'] == 3 and info['minor'] >= 8}")Expected Output
Python 3.X.Y\nFull version: 3.X.Y (main, ...)\nIs Python 3.8+: TrueHints
Hint 1: The `sys` module has `version_info` (a named tuple with major, minor, micro) and `version` (a full string).
Hint 2: `sys.version_info.major` gives 3, `sys.version_info.minor` gives the minor version. `sys.version` gives the complete version string with build info.
Write a function that identifies the exact Python binary that is executing the current script. Determine whether it is a symbolic link and find the real (resolved) path behind it. This is essential for debugging "which Python am I actually using?" issues.
import sys
import os
def identify_python():
"""Return a dict with the executable path, whether it's a symlink, and its real path."""
pass
result = identify_python()
print(f"Executable: {result['executable']}")
print(f"Is symlink: {result['is_symlink']}")
print(f"Real path: {result['real_path']}")
print(f"Same file: {result['executable'] == result['real_path']}")Solution
import sys
import os
def identify_python():
"""Return a dict with the executable path, whether it's a symlink, and its real path."""
executable = sys.executable
real_path = os.path.realpath(executable)
return {
"executable": executable,
"is_symlink": os.path.islink(executable),
"real_path": real_path,
}
result = identify_python()
print(f"Executable: {result['executable']}")
print(f"Is symlink: {result['is_symlink']}")
print(f"Real path: {result['real_path']}")
print(f"Same file: {result['executable'] == result['real_path']}")Why this matters:
On many systems, python3 is a symlink to a specific version like python3.11. When you install via pyenv or Homebrew, the symlink chain can be deep. sys.executable tells you the entry point, but os.path.realpath() resolves every symlink to the actual binary. This is the first thing to check when you get "module not found" errors despite having installed a package — you might be installing into one Python and running another.
import sys
import os
def identify_python():
"""Return a dict with the executable path, whether it's a symlink, and its real path."""
# Your code here
pass
result = identify_python()
print(f"Executable: {result['executable']}")
print(f"Is symlink: {result['is_symlink']}")
print(f"Real path: {result['real_path']}")
print(f"Same file: {result['executable'] == result['real_path']}")Expected Output
Executable: /path/to/python3\nIs symlink: True|False\nReal path: /path/to/real/python3\nSame file: True|FalseHints
Hint 1: `sys.executable` gives the absolute path to the Python interpreter currently running. `os.path.islink()` checks if a path is a symbolic link.
Hint 2: `os.path.realpath()` resolves all symbolic links and returns the canonical path. Compare it with `sys.executable` to see if you are running through a symlink.
Write a function that checks whether a virtual environment is currently active. Return a dictionary with the activation status, both prefix paths, and the venv name if one is active. Use the canonical sys.prefix vs sys.base_prefix comparison.
import sys
import os
def check_venv_status():
"""Check if a virtual environment is currently active. Return a status dict."""
pass
status = check_venv_status()
print(f"In virtual env: {status['in_venv']}")
print(f"sys.prefix: {status['prefix']}")
print(f"sys.base_prefix: {status['base_prefix']}")
if status['venv_name']:
print(f"Venv name: {status['venv_name']}")Solution
import sys
import os
def check_venv_status():
"""Check if a virtual environment is currently active. Return a status dict."""
in_venv = sys.prefix != sys.base_prefix
venv_name = None
if in_venv:
venv_name = os.path.basename(sys.prefix)
return {
"in_venv": in_venv,
"prefix": sys.prefix,
"base_prefix": sys.base_prefix,
"venv_name": venv_name,
}
status = check_venv_status()
print(f"In virtual env: {status['in_venv']}")
print(f"sys.prefix: {status['prefix']}")
print(f"sys.base_prefix: {status['base_prefix']}")
if status['venv_name']:
print(f"Venv name: {status['venv_name']}")Why this matters:
The sys.prefix != sys.base_prefix check is the most reliable way to detect a virtual environment. When you create a venv with python -m venv myenv, Python sets sys.prefix to the venv directory but leaves sys.base_prefix pointing to the system installation. The VIRTUAL_ENV environment variable is only set by the activation script (source myenv/bin/activate), so it may be absent if you invoke the venv's Python directly (./myenv/bin/python). The prefix comparison works in both cases.
import sys
import os
def check_venv_status():
"""Check if a virtual environment is currently active. Return a status dict."""
# Your code here
pass
status = check_venv_status()
print(f"In virtual env: {status['in_venv']}")
print(f"sys.prefix: {status['prefix']}")
print(f"sys.base_prefix: {status['base_prefix']}")
if status['venv_name']:
print(f"Venv name: {status['venv_name']}")Expected Output
In virtual env: True|False\nsys.prefix: /path/to/venv\nsys.base_prefix: /path/to/base/python\nVenv name: my-venv (if active)Hints
Hint 1: When a virtual environment is active, `sys.prefix` points to the venv directory while `sys.base_prefix` points to the system Python. They differ only inside a venv.
Hint 2: The `VIRTUAL_ENV` environment variable is set by the venv activation script. `os.path.basename()` on `sys.prefix` gives you the venv directory name.
Medium
Write a function that scans every directory in PATH and finds all Python executables (matching patterns like python, python3, python3.11, etc.). For each one, report its full path, whether it is a symlink, and its resolved real path. This simulates what which -a python3 does, but with more detail.
import os
import glob
import stat
def find_all_pythons():
"""Scan PATH directories for all Python executables. Return a list of dicts."""
pass
pythons = find_all_pythons()
print(f"Found {len(pythons)} Python installation(s):\n")
for p in pythons:
print(f" {p['path']}")
print(f" Symlink: {p['is_symlink']}, Real: {p['real_path']}")
print()Solution
import os
import glob
def find_all_pythons():
"""Scan PATH directories for all Python executables. Return a list of dicts."""
path_dirs = os.environ.get("PATH", "").split(os.pathsep)
patterns = ["python", "python3", "python3.*"]
seen_real = set()
results = []
for directory in path_dirs:
if not os.path.isdir(directory):
continue
for pattern in patterns:
for match in glob.glob(os.path.join(directory, pattern)):
if not os.path.isfile(match):
continue
if not os.access(match, os.X_OK):
continue
real = os.path.realpath(match)
# Deduplicate by real path
key = real
if key in seen_real:
continue
seen_real.add(key)
results.append({
"path": match,
"is_symlink": os.path.islink(match),
"real_path": real,
})
return results
pythons = find_all_pythons()
print(f"Found {len(pythons)} Python installation(s):\n")
for p in pythons:
print(f" {p['path']}")
print(f" Symlink: {p['is_symlink']}, Real: {p['real_path']}")
print()Why this matters:
Having multiple Python installations is the number one source of "I installed the package but Python can't find it." This script reveals every Python on your system, ordered by PATH priority. The first match is what runs when you type python3. If pyenv, Homebrew, and system Python all coexist, their PATH ordering determines which one wins. Deduplicating by realpath prevents counting /usr/bin/python3 -> /usr/bin/python3.11 as two separate installations.
import os
import glob
import stat
def find_all_pythons():
"""Scan PATH directories for all Python executables. Return a list of dicts."""
# Your code here
pass
pythons = find_all_pythons()
print(f"Found {len(pythons)} Python installation(s):\n")
for p in pythons:
print(f" {p['path']}")
print(f" Symlink: {p['is_symlink']}, Real: {p['real_path']}")
print()Expected Output
Found N Python installation(s):\n\n /usr/bin/python3\n Symlink: True, Real: /usr/bin/python3.X\n ...Hints
Hint 1: Split `os.environ["PATH"]` on `os.pathsep` to get each directory. Then use `glob.glob()` or `os.listdir()` to find files matching patterns like `python*` in each directory.
Hint 2: Check `os.path.isfile()` and `os.access(path, os.X_OK)` to confirm the file is an executable. Use `os.path.realpath()` to resolve symlinks and avoid counting the same binary twice.
Write a parser for requirements.txt content that extracts package names and version specifiers. Then write a validator that detects common issues: duplicate packages, missing version pins, and invalid version specifiers.
import re
SAMPLE_REQUIREMENTS = """
# Core dependencies
requests==2.31.0
flask>=2.0,<3.0
numpy
pandas==2.1.4
# Duplicate (should warn)
requests>=2.30.0
# Invalid operator
scipy~=1.11
"""
def parse_requirements(text):
"""Parse a requirements.txt string. Return list of dicts."""
pass
def validate_requirements(parsed):
"""Check for issues. Return list of warning strings."""
pass
parsed = parse_requirements(SAMPLE_REQUIREMENTS)
warnings = validate_requirements(parsed)
print(f"Parsed {len(parsed)} requirements\n")
for p in parsed:
status = "valid" if p['version_spec'] else "warning: no version pinned"
print(f" {p['name']} {p['version_spec']} [{status}]")
print(f"\nWarnings ({len(warnings)}):")
for w in warnings:
print(f" - {w}")Solution
import re
SAMPLE_REQUIREMENTS = """
# Core dependencies
requests==2.31.0
flask>=2.0,<3.0
numpy
pandas==2.1.4
# Duplicate (should warn)
requests>=2.30.0
# Invalid operator
scipy~=1.11
"""
def parse_requirements(text):
"""Parse a requirements.txt string. Return list of dicts."""
results = []
pattern = re.compile(r'^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(.*)?$')
valid_ops = {"==", ">=", "<=", "!=", ">", "<", "~=", "==="}
for line in text.strip().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
match = pattern.match(line)
if not match:
continue
name = match.group(1).strip()
version_spec = match.group(2).strip() if match.group(2) else ""
# Check if the version operators are valid
ops_valid = True
if version_spec:
specs = [s.strip() for s in version_spec.split(",")]
for spec in specs:
op_match = re.match(r'^(~=|===|==|!=|>=|<=|>|<)', spec)
if not op_match:
ops_valid = False
results.append({
"name": name,
"version_spec": version_spec,
"ops_valid": ops_valid,
})
return results
def validate_requirements(parsed):
"""Check for issues. Return list of warning strings."""
warnings = []
seen = {}
for req in parsed:
name_lower = req["name"].lower()
# Check for duplicates
if name_lower in seen:
seen[name_lower] += 1
else:
seen[name_lower] = 1
# Check for missing version
if not req["version_spec"]:
warnings.append(f"{req['name']}: no version constraint (not reproducible)")
# Check operator validity
if req["version_spec"] and not req["ops_valid"]:
warnings.append(f"{req['name']}: invalid version specifier '{req['version_spec']}'")
# Report duplicates
for name, count in seen.items():
if count > 1:
warnings.append(f"{name}: appears {count} times (duplicate)")
return warnings
parsed = parse_requirements(SAMPLE_REQUIREMENTS)
warnings = validate_requirements(parsed)
print(f"Parsed {len(parsed)} requirements\n")
for p in parsed:
status = "valid" if p['version_spec'] else "warning: no version pinned"
print(f" {p['name']} {p['version_spec']} [{status}]")
print(f"\nWarnings ({len(warnings)}):")
for w in warnings:
print(f" - {w}")Why this matters:
A requirements.txt without pinned versions (numpy instead of numpy==1.26.2) is the leading cause of "it works on my machine." This parser catches the three most common requirements file issues: (1) unpinned versions that make builds non-reproducible, (2) duplicate packages with conflicting version specs, and (3) invalid operators. Production-grade tools like pip-compile (from pip-tools) solve this by generating fully pinned files from abstract dependencies.
import re
def parse_requirements(text):
"""Parse a requirements.txt string. Return list of dicts with name, operator, version, valid flag."""
# Your code here
pass
def validate_requirements(parsed):
"""Check for issues: duplicates, missing versions, invalid operators. Return list of warnings."""
# Your code here
passExpected Output
Parsed 6 requirements\n requests ==2.31.0 [valid]\n flask >=2.0,<3.0 [valid]\n numpy [warning: no version pinned]\n ...\nWarnings:\n - numpy: no version constraint (not reproducible)\n - requests: appears 2 times (duplicate)Hints
Hint 1: A requirements line format is `package_name[extras]` followed by optional version specifiers like `==1.0`, `>=2.0,<3.0`. Comments start with `#` and blank lines should be skipped.
Hint 2: Use a regex like `^([a-zA-Z0-9_-]+)\s*(.*)$` to separate package name from version spec. Track seen names in a set to catch duplicates.
Build a health checker that examines the current Python environment and reports on: Python version, executable path, whether a venv is active, pip availability, site-packages location, number of installed packages, and whether site-packages is writable. Output a formatted report with OK/WARN status for each check.
import sys
import os
import sysconfig
import subprocess
def check_venv_health():
"""Run health checks on the current Python environment. Return a report dict."""
pass
report = check_venv_health()
print("=== Environment Health Report ===")
for key, value in report.items():
label = key.replace("_", " ").ljust(20)
status = value.get("status", "")
detail = value.get("detail", "")
print(f"{label}{detail} [{status}]")Solution
import sys
import os
import sysconfig
import subprocess
def check_venv_health():
"""Run health checks on the current Python environment. Return a report dict."""
report = {}
# 1. Python version
ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
report["python_version"] = {
"detail": ver,
"status": "OK" if sys.version_info >= (3, 8) else "WARN",
}
# 2. Executable path
exe = sys.executable
report["executable"] = {
"detail": exe,
"status": "OK" if os.path.exists(exe) else "FAIL",
}
# 3. Virtual environment status
in_venv = sys.prefix != sys.base_prefix
report["venv_active"] = {
"detail": str(in_venv),
"status": "OK" if in_venv else "WARN",
}
# 4. pip availability
try:
result = subprocess.run(
[sys.executable, "-m", "pip", "--version"],
capture_output=True, text=True, timeout=10
)
pip_ok = result.returncode == 0
except (subprocess.TimeoutExpired, FileNotFoundError):
pip_ok = False
report["pip_available"] = {
"detail": str(pip_ok),
"status": "OK" if pip_ok else "FAIL",
}
# 5. site-packages location
paths = sysconfig.get_paths()
site_packages = paths.get("purelib", "unknown")
report["site_packages"] = {
"detail": site_packages,
"status": "OK" if os.path.isdir(site_packages) else "FAIL",
}
# 6. Package count
pkg_count = 0
if os.path.isdir(site_packages):
pkg_count = len([
d for d in os.listdir(site_packages)
if d.endswith(".dist-info")
])
report["package_count"] = {
"detail": f"{pkg_count} installed",
"status": "OK" if pkg_count > 0 else "WARN",
}
# 7. Writable
writable = os.access(site_packages, os.W_OK) if os.path.isdir(site_packages) else False
report["writable"] = {
"detail": str(writable),
"status": "OK" if writable else "WARN",
}
return report
report = check_venv_health()
print("=== Environment Health Report ===")
for key, value in report.items():
label = key.replace("_", " ").ljust(20)
status = value.get("status", "")
detail = value.get("detail", "")
print(f"{label}{detail} [{status}]")Why this matters:
When something goes wrong with package installations, you need a systematic way to diagnose the environment. This health checker covers the most common failure modes: wrong Python version, pip not installed (common in minimal Docker images), site-packages not writable (permission issues), and no venv active (risking global pollution). The .dist-info directory count is how pip itself tracks installed packages.
import sys
import os
import sysconfig
import subprocess
def check_venv_health():
"""Run health checks on the current Python environment. Return a report dict."""
# Your code here
passExpected Output
=== Environment Health Report ===\nPython version: 3.X.Y [OK]\nExecutable: /path/to/python [OK]\nVenv active: True|False [OK|WARN]\npip available: True [OK]\nsite-packages: /path/to/site-packages [OK]\nPackage count: N installed [OK]\nWritable: True [OK]Hints
Hint 1: Use `sysconfig.get_paths()` to find the `purelib` path (where site-packages live). Check if it exists with `os.path.isdir()` and if it is writable with `os.access(path, os.W_OK)`.
Hint 2: To check if pip is available without importing it, use `subprocess.run([sys.executable, "-m", "pip", "--version"])` and check the return code. Count packages with `os.listdir()` on site-packages, filtering for `.dist-info` directories.
Write a diagnostic tool that scans for the most common Python installation issues. Check for: Python version below 3.8, no virtual environment active, pip not available, python command missing (only python3 exists), and multiple conflicting Python installations on PATH. Report each check as PASS, WARN, or FAIL.
import sys
import os
import shutil
import subprocess
def diagnose_installation():
"""Detect common Python installation problems. Return a list of issue dicts."""
pass
issues = diagnose_installation()
print("=== Installation Diagnostic ===\n")
for issue in issues:
icon = {"PASS": "PASS", "WARN": "WARN", "FAIL": "FAIL"}[issue["level"]]
print(f"[{icon}] {issue['message']}")
if issue.get("detail"):
print(f" {issue['detail']}")Solution
import sys
import os
import shutil
import subprocess
import glob
def diagnose_installation():
"""Detect common Python installation problems. Return a list of issue dicts."""
issues = []
# 1. Python version check
if sys.version_info >= (3, 8):
issues.append({
"level": "PASS",
"message": f"Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} detected",
})
else:
issues.append({
"level": "FAIL",
"message": f"Python {sys.version_info.major}.{sys.version_info.minor} is below minimum 3.8",
"detail": "Upgrade Python. Many modern packages require 3.8+.",
})
# 2. Virtual environment check
in_venv = sys.prefix != sys.base_prefix
if in_venv:
issues.append({
"level": "PASS",
"message": "Virtual environment is active",
"detail": f"venv: {os.path.basename(sys.prefix)}",
})
else:
issues.append({
"level": "WARN",
"message": "No virtual environment active",
"detail": "Run: python -m venv .venv && source .venv/bin/activate",
})
# 3. pip availability
try:
result = subprocess.run(
[sys.executable, "-m", "pip", "--version"],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
pip_ver = result.stdout.strip().split("\n")[0]
issues.append({"level": "PASS", "message": "pip is available", "detail": pip_ver})
else:
issues.append({
"level": "FAIL",
"message": "pip is not working",
"detail": "Run: python -m ensurepip --upgrade",
})
except Exception:
issues.append({
"level": "FAIL",
"message": "pip is not available",
"detail": "Run: python -m ensurepip --upgrade",
})
# 4. python vs python3 command
python_cmd = shutil.which("python")
python3_cmd = shutil.which("python3")
if python_cmd and python3_cmd:
# Check if they point to the same binary
real_py = os.path.realpath(python_cmd) if python_cmd else None
real_py3 = os.path.realpath(python3_cmd) if python3_cmd else None
if real_py == real_py3:
issues.append({"level": "PASS", "message": "'python' and 'python3' both resolve to the same binary"})
else:
issues.append({
"level": "WARN",
"message": "'python' and 'python3' resolve to different binaries",
"detail": f"python -> {real_py}, python3 -> {real_py3}",
})
elif python3_cmd and not python_cmd:
issues.append({
"level": "WARN",
"message": "'python' command not found (only 'python3' exists)",
"detail": "Some tools expect 'python'. Consider a symlink or alias.",
})
elif not python3_cmd:
issues.append({
"level": "FAIL",
"message": "'python3' command not found on PATH",
})
# 5. Multiple Python versions on PATH
path_dirs = os.environ.get("PATH", "").split(os.pathsep)
python_binaries = set()
for d in path_dirs:
if not os.path.isdir(d):
continue
for entry in glob.glob(os.path.join(d, "python3.*")):
if os.path.isfile(entry) and os.access(entry, os.X_OK):
# Extract just the version part
basename = os.path.basename(entry)
if not basename.endswith(".py"):
python_binaries.add(entry)
if len(python_binaries) > 1:
issues.append({
"level": "WARN",
"message": f"Multiple Python 3.x versions found on PATH ({len(python_binaries)})",
"detail": ", ".join(sorted(python_binaries)),
})
else:
issues.append({"level": "PASS", "message": "No conflicting Python versions on PATH"})
return issues
issues = diagnose_installation()
print("=== Installation Diagnostic ===\n")
for issue in issues:
icon = {"PASS": "PASS", "WARN": "WARN", "FAIL": "FAIL"}[issue["level"]]
print(f"[{icon}] {issue['message']}")
if issue.get("detail"):
print(f" {issue['detail']}")Why this matters:
This is the script you wish you had the first time you spent an hour debugging "ModuleNotFoundError" on a fresh machine. The five checks cover the most frequent causes: (1) ancient Python version that is missing modern features, (2) installing packages globally instead of in a venv, (3) pip not available in minimal Linux installs or Docker images, (4) python not existing as a command (common on Ubuntu), and (5) PATH ordering causing the wrong Python to run. Save a version of this script and run it on every new machine.
import sys
import os
import shutil
def diagnose_installation():
"""Detect common Python installation problems. Return a list of issue dicts."""
# Your code here
passExpected Output
=== Installation Diagnostic ===\n[PASS] Python 3.8+ detected\n[WARN] No virtual environment active\n[PASS] pip is available\n[WARN] Multiple python3 found in PATH\n...Hints
Hint 1: Check for these common issues: Python version too old (below 3.8), no venv active, pip missing, `python` command not found (only `python3` exists), and multiple Python versions on PATH.
Hint 2: Use `shutil.which()` to check if `python` and `python3` resolve to executables. Scan PATH directories for multiple python3.* binaries to detect version conflicts.
Hard
Build a comprehensive environment fingerprint tool that captures everything needed to reproduce a Python environment: Python version and implementation, platform details, all configured paths, installed packages with versions, and a SHA-256 hash of the entire fingerprint for quick comparison. Output should be valid JSON.
import sys
import os
import platform
import sysconfig
import hashlib
import json
def environment_fingerprint():
"""Generate a complete fingerprint of the current Python environment."""
pass
fp = environment_fingerprint()
print(json.dumps(fp, indent=2))
print(f"\nFingerprint hash: {fp['hash']}")Solution
import sys
import os
import platform
import sysconfig
import hashlib
import json
def get_installed_packages():
"""Scan site-packages for installed packages by reading .dist-info metadata."""
packages = []
paths = sysconfig.get_paths()
site_packages = paths.get("purelib", "")
if not os.path.isdir(site_packages):
return packages
for entry in sorted(os.listdir(site_packages)):
if entry.endswith(".dist-info"):
# Format: package_name-version.dist-info
parts = entry[:-len(".dist-info")]
# The last hyphen-separated segment with digits is the version
segments = parts.split("-")
if len(segments) >= 2:
name = "-".join(segments[:-1])
version = segments[-1]
packages.append({"name": name, "version": version})
else:
packages.append({"name": parts, "version": "unknown"})
return packages
def environment_fingerprint():
"""Generate a complete fingerprint of the current Python environment."""
# Python info
python_info = {
"version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
"full_version": sys.version,
"implementation": platform.python_implementation(),
"compiler": platform.python_compiler(),
"executable": sys.executable,
"prefix": sys.prefix,
"base_prefix": sys.base_prefix,
"in_venv": sys.prefix != sys.base_prefix,
}
# Platform info
platform_info = {
"system": platform.system(),
"release": platform.release(),
"machine": platform.machine(),
"processor": platform.processor(),
"platform": platform.platform(),
}
# Path configuration
all_paths = sysconfig.get_paths()
path_info = {
"prefix": all_paths.get("prefix", ""),
"stdlib": all_paths.get("stdlib", ""),
"site_packages": all_paths.get("purelib", ""),
"include": all_paths.get("include", ""),
"scripts": all_paths.get("scripts", ""),
"data": all_paths.get("data", ""),
}
# Installed packages
packages = get_installed_packages()
# Build the fingerprint (without hash)
fingerprint = {
"python": python_info,
"platform": platform_info,
"paths": path_info,
"packages": packages,
"package_count": len(packages),
}
# Generate reproducible hash
canonical = json.dumps(fingerprint, sort_keys=True, separators=(",", ":"))
fp_hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
fingerprint["hash"] = f"sha256:{fp_hash[:16]}"
return fingerprint
fp = environment_fingerprint()
print(json.dumps(fp, indent=2))
print(f"\nFingerprint hash: {fp['hash']}")Why this matters:
Environment fingerprints are how teams debug "works on my machine" problems. When a CI build fails but your local run passes, comparing fingerprints immediately reveals the difference: a different Python micro version, a missing package, a different platform. The SHA-256 hash lets you embed a single string in logs or Docker labels: if two hashes match, the environments are identical. This pattern is used in production by tools like pip freeze and conda env export, but this version captures platform details they miss.
import sys
import os
import platform
import sysconfig
import hashlib
import json
def environment_fingerprint():
"""Generate a complete fingerprint of the current Python environment.
Returns a dict that can be serialized to JSON for reproducibility tracking.
"""
# Your code here
passExpected Output
{\n "python": { "version": "3.X.Y", "implementation": "CPython", ... },\n "platform": { "system": "Linux", "machine": "x86_64", ... },\n "paths": { "prefix": "...", "site_packages": "...", ... },\n "packages": [ { "name": "pip", "version": "..." }, ... ],\n "hash": "sha256:abc123..."\n}Hints
Hint 1: Use `platform` module for OS info (`platform.system()`, `platform.machine()`, `platform.python_implementation()`). Use `sysconfig.get_paths()` for all Python paths. For packages, scan the site-packages directory for `.dist-info` folders.
Hint 2: To generate a reproducible hash, serialize the sorted fingerprint dict to a JSON string and hash it with `hashlib.sha256()`. This lets you compare environments by a single hash value.
Build a dependency conflict detector. Given multiple sets of requirements (simulating different projects or dependency trees), parse version specifiers, and determine whether they are mutually satisfiable. Report which packages have conflicts and why.
import re
# Simulated dependency requirements from two projects
PROJECT_A = {
"numpy": ">=1.24,<1.26",
"requests": ">=2.28",
"pandas": ">=2.0,<2.2",
"flask": "==2.3.2",
}
PROJECT_B = {
"numpy": ">=1.26",
"requests": ">=2.25,<3.0",
"pandas": ">=1.5,<2.1",
"flask": ">=2.0,<3.0",
}
def parse_version(version_str):
"""Parse a version string into a comparable tuple of ints."""
pass
def check_constraints_compatible(specs_list):
"""Check if a list of version spec strings can be simultaneously satisfied.
Return (is_compatible, explanation)."""
pass
def detect_conflicts(project_deps):
"""Given a dict of project_name -> {package: spec}, detect conflicts."""
pass
conflicts = detect_conflicts({"project-a": PROJECT_A, "project-b": PROJECT_B})
print("=== Dependency Conflict Report ===\n")
for pkg, result in sorted(conflicts.items()):
if result["conflict"]:
print(f"CONFLICT: {pkg}")
else:
print(f"OK: {pkg}")
for detail in result["details"]:
print(f" {detail}")
print()Solution
import re
PROJECT_A = {
"numpy": ">=1.24,<1.26",
"requests": ">=2.28",
"pandas": ">=2.0,<2.2",
"flask": "==2.3.2",
}
PROJECT_B = {
"numpy": ">=1.26",
"requests": ">=2.25,<3.0",
"pandas": ">=1.5,<2.1",
"flask": ">=2.0,<3.0",
}
def parse_version(version_str):
"""Parse a version string into a comparable tuple of ints."""
parts = version_str.strip().split(".")
result = []
for p in parts:
# Extract digits only
digits = re.match(r'(\d+)', p)
result.append(int(digits.group(1)) if digits else 0)
# Pad to at least 3 components
while len(result) < 3:
result.append(0)
return tuple(result)
def parse_spec(spec_str):
"""Parse a version spec string like '>=1.24,<1.26' into a list of (operator, version_tuple)."""
constraints = []
parts = [s.strip() for s in spec_str.split(",")]
pattern = re.compile(r'^(===|==|~=|!=|>=|<=|>|<)(.+)$')
for part in parts:
match = pattern.match(part)
if match:
op = match.group(1)
ver = parse_version(match.group(2))
constraints.append((op, ver))
return constraints
def find_bounds(all_constraints):
"""Given a flat list of (op, ver) constraints, compute the effective bounds.
Returns (lower_bound, lower_inclusive, upper_bound, upper_inclusive, eq_value)."""
lower = (0, 0, 0)
lower_inclusive = True
upper = (999, 999, 999)
upper_inclusive = True
eq_values = []
for op, ver in all_constraints:
if op == "==":
eq_values.append(ver)
elif op == ">=":
if ver > lower or (ver == lower and not lower_inclusive):
lower = ver
lower_inclusive = True
elif op == ">":
if ver > lower or (ver == lower):
lower = ver
lower_inclusive = False
elif op == "<=":
if ver < upper or (ver == upper and not upper_inclusive):
upper = ver
upper_inclusive = True
elif op == "<":
if ver < upper or (ver == upper):
upper = ver
upper_inclusive = False
elif op == "!=":
pass # Exclude checks are harder; skip for this exercise
elif op == "~=":
# ~=1.11 means >=1.11,<2.0 (compatible release)
if ver > lower:
lower = ver
lower_inclusive = True
compat_upper = (ver[0] + 1, 0, 0) if len(ver) <= 2 else (ver[0], ver[1] + 1, 0)
if compat_upper < upper:
upper = compat_upper
upper_inclusive = False
return lower, lower_inclusive, upper, upper_inclusive, eq_values
def check_constraints_compatible(all_specs):
"""Check if multiple spec strings can be simultaneously satisfied."""
all_constraints = []
for spec in all_specs:
all_constraints.extend(parse_spec(spec))
lower, lower_incl, upper, upper_incl, eq_values = find_bounds(all_constraints)
# If there are equality pins, check they satisfy all bounds
if eq_values:
for eq in eq_values:
satisfies_lower = eq > lower or (eq == lower and lower_incl)
satisfies_upper = eq < upper or (eq == upper and upper_incl)
if not (satisfies_lower and satisfies_upper):
return False, f"pinned {eq} outside range [{lower}, {upper}]"
# Check all eq values are the same
if len(set(eq_values)) > 1:
return False, f"conflicting pins: {eq_values}"
return True, f"pinned at {eq_values[0]}, within bounds"
# Check if range is non-empty
if lower > upper:
return False, f"lower bound {lower} > upper bound {upper}"
if lower == upper and not (lower_incl and upper_incl):
return False, f"empty range at {lower} (exclusive bound)"
return True, f"satisfiable range: [{'>=' if lower_incl else '>'}{lower}, {'<=' if upper_incl else '<'}{upper}]"
def detect_conflicts(project_deps):
"""Given a dict of project_name -> {package: spec}, detect conflicts."""
# Collect all packages across all projects
all_packages = set()
for proj_reqs in project_deps.values():
all_packages.update(proj_reqs.keys())
results = {}
for pkg in sorted(all_packages):
specs = []
details = []
for proj_name, proj_reqs in project_deps.items():
if pkg in proj_reqs:
spec = proj_reqs[pkg]
specs.append(spec)
details.append(f"{proj_name} requires: {pkg}{spec}")
if len(specs) <= 1:
results[pkg] = {"conflict": False, "details": details + ["Only one source, no conflict possible."]}
continue
is_ok, explanation = check_constraints_compatible(specs)
details.append(f"{'Compatible' if is_ok else 'INCOMPATIBLE'}: {explanation}")
results[pkg] = {"conflict": not is_ok, "details": details}
return results
conflicts = detect_conflicts({"project-a": PROJECT_A, "project-b": PROJECT_B})
print("=== Dependency Conflict Report ===\n")
for pkg, result in sorted(conflicts.items()):
if result["conflict"]:
print(f"CONFLICT: {pkg}")
else:
print(f"OK: {pkg}")
for detail in result["details"]:
print(f" {detail}")
print()Why this matters:
Dependency conflicts (also called "dependency hell") are the bane of Python packaging. When project A needs numpy>=1.24,<1.26 and project B needs numpy>=1.26, there is no version of numpy that satisfies both. Tools like pip attempt to resolve this at install time but can fail silently or pick a version that breaks one project. Understanding how version constraints form intervals and how to check interval overlap is foundational to understanding what pip, poetry, and conda do under the hood. The key insight: constraints define a range, and compatibility means the intersection of all ranges is non-empty.
import re
import os
import sysconfig
def parse_version(version_str):
"""Parse a version string into a comparable tuple."""
# Your code here
pass
def check_requirement(installed_version, spec_str):
"""Check if an installed version satisfies a requirement spec like '>=1.0,<2.0'."""
# Your code here
pass
def detect_conflicts(requirements_sets):
"""Given multiple sets of requirements, detect version conflicts."""
# Your code here
passExpected Output
Checking dependency sets for conflicts...\n\nConflict detected for 'numpy':\n project-a requires: numpy>=1.24,<1.26\n project-b requires: numpy>=1.26\n These constraints cannot be satisfied simultaneously.\n\nNo conflict for 'requests':\n project-a requires: requests>=2.28\n project-b requires: requests>=2.25,<3.0\n Compatible range exists.Hints
Hint 1: Parse version strings by splitting on `.` and converting to integers: `(1, 24, 0)`. Compare tuples directly since Python compares tuples element-by-element. Handle missing micro versions by defaulting to 0.
Hint 2: For each package, collect all constraints from all requirement sets. Then check if there exists any version that satisfies ALL constraints simultaneously. If the maximum lower bound exceeds the minimum upper bound, there is a conflict.
Build a cross-platform Python path resolver that maps out where Python stores its standard library, site-packages, scripts, and headers on the current platform. Then implement a module finder that locates where any given module would be loaded from (file path, built-in, or frozen). Handle the differences between POSIX and Windows path schemes.
import sys
import os
import sysconfig
import platform
import importlib.util
from pathlib import Path
def resolve_python_paths():
"""Build a complete map of Python paths on the current platform."""
pass
def find_module_location(module_name):
"""Find where Python would load a module from. Return a description string."""
pass
# Part 1: Path map
path_map = resolve_python_paths()
print(f"=== Python Path Map ({platform.system()}) ===\n")
for key, value in path_map.items():
label = key.ljust(18)
print(f"{label}{value}")
# Part 2: Module lookup
print("\n=== Module Lookup ===\n")
test_modules = ["os", "json", "sys", "collections", "importlib", "pathlib"]
for mod in test_modules:
location = find_module_location(mod)
print(f"{mod.ljust(16)}{location}")Solution
import sys
import os
import sysconfig
import platform
import importlib.util
from pathlib import Path
def resolve_python_paths():
"""Build a complete map of Python paths on the current platform."""
scheme = sysconfig.get_default_scheme()
paths = sysconfig.get_paths()
is_venv = sys.prefix != sys.base_prefix
result = {
"scheme": scheme,
"platform": platform.system(),
"prefix": sys.prefix,
"base_prefix": sys.base_prefix,
"in_venv": str(is_venv),
"stdlib": paths.get("stdlib", "N/A"),
"site_packages": paths.get("purelib", "N/A"),
"platlib": paths.get("platlib", "N/A"),
"scripts": paths.get("scripts", "N/A"),
"include": paths.get("include", "N/A"),
"data": paths.get("data", "N/A"),
}
# Add sys.path for full search order
result["sys.path_count"] = str(len(sys.path))
# Platform-specific notes
system = platform.system()
if system == "Windows":
result["note"] = "Windows uses 'nt' scheme; Scripts/ instead of bin/"
elif system == "Darwin":
result["note"] = "macOS may have Framework builds with different paths"
else:
result["note"] = "Linux uses posix_prefix; check for dist-packages on Debian/Ubuntu"
# Check for Debian/Ubuntu dist-packages vs site-packages
if system == "Linux":
dist_packages = os.path.join(sys.prefix, "lib",
f"python{sys.version_info.major}.{sys.version_info.minor}",
"dist-packages")
result["dist_packages_exists"] = str(os.path.isdir(dist_packages))
return result
def find_module_location(module_name):
"""Find where Python would load a module from. Return a description string."""
# Check if it's a built-in module
if module_name in sys.builtin_module_names:
return "built-in (compiled into interpreter)"
# Try to find the module spec
try:
spec = importlib.util.find_spec(module_name)
except (ModuleNotFoundError, ValueError):
return "NOT FOUND"
if spec is None:
return "NOT FOUND"
# Check if it's a frozen module
if spec.origin == "frozen":
return "frozen (embedded in interpreter)"
# Check if it's a namespace package (no origin)
if spec.origin is None:
if spec.submodule_search_locations:
locations = list(spec.submodule_search_locations)
return f"namespace package: {locations[0]}"
return "namespace package (no fixed location)"
# Regular file-based module
origin = spec.origin
# Determine if it's stdlib or third-party
stdlib_path = sysconfig.get_paths()["stdlib"]
site_path = sysconfig.get_paths()["purelib"]
if origin.startswith(stdlib_path):
category = "stdlib"
elif origin.startswith(site_path):
category = "third-party"
else:
category = "other"
# Check if it's a package (directory) or single file
is_package = spec.submodule_search_locations is not None
kind = "package" if is_package else "module"
return f"{origin} [{category} {kind}]"
# Part 1: Path map
path_map = resolve_python_paths()
print(f"=== Python Path Map ({platform.system()}) ===\n")
for key, value in path_map.items():
label = key.ljust(18)
print(f"{label} {value}")
# Part 2: Module lookup
print("\n=== Module Lookup ===\n")
test_modules = ["os", "json", "sys", "collections", "importlib", "pathlib"]
for mod in test_modules:
location = find_module_location(mod)
print(f"{mod.ljust(16)}{location}")
# Bonus: Show full sys.path search order
print("\n=== sys.path Search Order ===\n")
for i, p in enumerate(sys.path):
exists = "exists" if os.path.exists(p) else "MISSING"
print(f" [{i}] {p} ({exists})")Why this matters:
Python's import system searches sys.path in order, and the exact paths differ dramatically across platforms. Windows uses Scripts\ where POSIX uses bin/. Debian/Ubuntu add a dist-packages directory alongside site-packages. macOS Framework builds store everything under /Library/Frameworks. Virtual environments override sys.prefix to redirect imports to an isolated directory.
Understanding this path resolution is critical for debugging imports. When import numpy fails, the answer is always in this chain: (1) which Python is running, (2) what is its sys.path, (3) where are packages actually installed. The importlib.util.find_spec() call is exactly what Python's import machinery uses internally — it is the authoritative answer to "where will this module come from?"
import sys
import os
import sysconfig
import platform
from pathlib import Path
def resolve_python_paths():
"""Build a complete map of Python paths on the current platform.
Handle differences between Windows, macOS, and Linux.
Return a structured dict.
"""
# Your code here
pass
def find_python_file(module_name):
"""Given a module name, find where Python would load it from.
Return the file path or None.
"""
# Your code here
passExpected Output
=== Python Path Map (macOS/Linux/Windows) ===\nScheme: posix_prefix | nt\nPrefix: /usr/local | C:\\Python311\nStdlib: /usr/local/lib/python3.11 | C:\\Python311\\Lib\nSite-packages: ...lib/python3.11/site-packages | ...\\Lib\\site-packages\nScripts/bin: /usr/local/bin | C:\\Python311\\Scripts\nHeaders: .../include/python3.11 | ...\\Include\n\n=== Module Lookup ===\nos: /usr/local/lib/python3.11/os.py\njson: /usr/local/lib/python3.11/json/__init__.py\nsys: built-inHints
Hint 1: Use `sysconfig.get_default_scheme()` to detect the platform scheme (e.g., `posix_prefix` on Linux/macOS, `nt` on Windows). `sysconfig.get_paths()` returns all paths for the current scheme.
Hint 2: To find where a module lives, use `importlib.util.find_spec(module_name)`. The returned spec object has an `origin` attribute with the file path, or it is None for built-in modules.
