In 2018, the PhantomJS maintainer posted a short message on GitHub:
"I am stepping down as maintainer. Slimer.js is mostly unmaintained. It's time for us to move on."
Headless Chrome had arrived. Everyone moved on. PhantomJS was officially dead.
But here's the thing nobody talked about: Chrome still isn't a Python package.
The Problem: Browser Automation Should Be pip install
Python has a philosophy — batteries included. You pip install requests and you have HTTP. You pip install numpy and you have numerical computing. Everything arrives as a package, versioned, isolated, reproducible.
So why does browser automation still require you to leave Python's ecosystem entirely?
-
playwright installdownloads a 300MB Chromium binary outside your virtualenv - Puppeteer requires Node.js and npm
- Selenium requires a separately installed browser and a matching ChromeDriver
These tools are powerful — but they're not Pythonic. They break the contract: one environment, one install, everything works.
I wanted something different. I wanted to write:
pip install phasma
And have a fully working headless browser. No apt. No npm. No system packages. No setup steps outside of pip. Just Python.
That's phasma — and PhantomJS, it turns out, was the perfect engine to build it on. A self-contained binary with a full WebKit engine that ships inside the wheel. The ecosystem had abandoned it, but the binary never stopped working.
What Is Phasma?
Phasma is a Python package that wraps PhantomJS with a modern, Playwright-like async API. The PhantomJS binary ships inside the wheel — no system packages, no Node.js, no Chromium, nothing. Just:
pip install phasma
And it works. On Linux, macOS, Windows. In Docker containers with no internet access. On HPC clusters. On that one server where the sysadmin says no.
A note on the name: In Star Wars, Captain Phasma is a chrome-armored stormtrooper who everyone thought was dead after The Force Awakens — and then came back in The Last Jedi. It felt like the right name for a package that resurrects a browser engine everyone declared dead. Also, PhantomJS is literally a phantom. It was right there.
The Real Use Case: Running DOM-dependent JavaScript Anywhere
Here's the concrete problem I kept running into. You have a JavaScript library that needs a real DOM to work. Maybe it's a charting library, a diagram renderer, a scraper, or just some legacy code that uses document.querySelector.
You want to run it from Python. Your options:
A real example from my own work: I needed to run Mermaid.js — the diagram-as-code library — from Python to generate SVG/PNG diagrams. Mermaid.js uses the DOM heavily. The official CLI (mmdc) requires Node.js. So I built mmdc for Python on top of phasma:
import asyncio
from phasma.svg import SvgRenderer
MERMAID_DIAGRAM = """
graph TD
A[pip install phasma] --> B[Import in Python]
B --> C[Launch Browser]
C --> D[Run any JS with DOM]
D --> E[Get results back]
"""
async def render_mermaid(diagram_code: str) -> bytes:
# Inject Mermaid.js from CDN, render the diagram, export as PNG
async with phasma.Browser() as browser:
page = await browser.new_page()
html = f"""
<html>
<body>
<div class="mermaid">{diagram_code}</div>
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script>mermaid.initialize({{ startOnLoad: true }});</script>
</body>
</html>"""
await page.goto(html)
await page.wait_for_selector(".mermaid svg", timeout=5000)
svg = await page.inner_html(".mermaid")
async with SvgRenderer() as r:
return await r.to_png(svg)
No Node.js. No npm install mermaid. Just Python.
How It Works Under the Hood
The old approach (every existing PhantomJS wrapper) looked like this:
Phasma works differently. When you call launch(), it starts one persistent PhantomJS process that runs a tiny HTTP server:
The result: ~9ms per operation instead of seconds. The same pattern Selenium WebDriver has used for years — I just applied it to PhantomJS.
The API
Phasma's API is deliberately close to Playwright so there's almost no learning curve:
import asyncio
import phasma
async def main():
browser = await phasma.launch()
try:
page = await browser.new_page()
# navigate
await page.goto("https://example.com")
# extract data
title = await page.evaluate("document.title")
heading = await page.text_content("h1")
links = await page.evaluate("""
Array.from(document.querySelectorAll('a'))
.map(a => ({ text: a.textContent, href: a.href }))
""")
# interact
await page.fill("#search", "phasma")
await page.click("#submit")
await page.wait_for_selector(".results")
# output
await page.screenshot("shot.png")
await page.pdf("page.pdf")
finally:
await browser.close()
asyncio.run(main())
SVG Rendering — Also Batteries Included
One thing I needed constantly: converting SVG files to PNG/PDF without installing Inkscape or cairosvg. Phasma handles this too, with a dedicated SvgRenderer class:
from phasma.svg import SvgRenderer
async with SvgRenderer() as r:
# basic conversion
png = await r.to_png("diagram.svg")
jpg = await r.to_jpeg("<svg ...>...</svg>")
pdf = await r.to_pdf("chart.svg")
# scale up for high-resolution export
png_2x = await r.to_png("icon.svg", scale=2.0) # 2× resolution
png_4x = await r.to_png("icon.svg", scale=4.0) # 4× resolution
# PDF that fits the SVG exactly — no A4 borders
pdf = await r.to_pdf("diagram.svg") # paper = SVG dimensions
# or standard paper size
pdf = await r.to_pdf("diagram.svg", pdf_format="A4")
One SvgRenderer = one PhantomJS process reused for all conversions. Batch-convert 100 SVGs and PhantomJS starts exactly once.
Running Any DOM-dependent JS: The General Pattern
This is the core superpower. Any JavaScript that needs a browser environment:
import asyncio
import phasma
# Your JS library that needs window, document, etc.
YOUR_LIBRARY_JS = """
// imagine this is Chart.js, D3, Mermaid, or any DOM library
var result = someLibraryThatNeedsDOM.process(inputData);
result // last expression is returned
"""
async def run_js_with_dom(js_code: str, html_context: str = "<html><body></body></html>"):
browser = await phasma.launch()
try:
page = await browser.new_page()
# set up the HTML context your JS needs
import tempfile
from pathlib import Path
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f:
f.write(html_context)
await page.goto(Path(f.name).as_uri())
# inject and run your library
return await page.evaluate(js_code)
finally:
await browser.close()
No Node.js. No Chrome. No system dependencies. Just Python and a pip install.
Benchmark
xychart-beta horizontal
title "Time per operation (ms) — lower is better"
x-axis ["goto()", "10× ops", "100× ops"]
y-axis "milliseconds" 0 --> 3000
bar [2500, 25000, 250000]
bar [9, 90, 900]
🔵 Old approach (per-process) — 🟣 Phasma (persistent session)
The difference is entirely startup cost. With phasma, PhantomJS starts once and stays alive.
Installation & Quick Start
pip install phasma
That's it. The PhantomJS binary is bundled in the wheel.
import asyncio
import phasma
async def main():
browser = await phasma.launch()
page = await browser.new_page()
await page.goto("https://example.com")
print(await page.evaluate("document.title"))
await browser.close()
asyncio.run(main())
Who Is This For?
- You're on a server where
apt/yumis locked - You're building a minimal Docker image and don't want to pull in Chrome (~300MB)
- You need to run DOM-dependent JavaScript from Python without Node.js
- You want to convert SVG files without system dependencies
- You need a headless browser that arrives via
pipand nothing else
Playwright and Puppeteer are better tools for modern web automation — if you can install them. Phasma exists for the cases where you can't.
Links
-
PyPI:
pip install phasma - GitHub: github.com/MohammadRaziei/phasma
- Author: Mohammad Raziei




Top comments (0)