yaspin renders animated terminal spinners with 100+ built-in styles. pip install yaspin. Basic: from yaspin import yaspin; with yaspin(text="Loading") as sp: time.sleep(2). Custom spinner: from yaspin.spinners import Spinners; with yaspin(Spinners.earth) as sp: .... Text: sp.text = "Step 2". Color: with yaspin(color="green") as sp:. On-color bg: yaspin(on_color="on_white"). Attrs: yaspin(attrs=["bold"]). Side: yaspin(text="Loading", side="right") — spinner on right. Outcome: sp.ok("✔"). sp.fail("✗"). sp.write("log line") — print without breaking spinner. Timer: yaspin(timer=True) — shows elapsed time. Hidden: with sp.hidden(): print("clean output"). Signal map: from yaspin.core import SIGMAP; yaspin(sigmap={signal.SIGTERM: sp.fail}). Spinner object: from yaspin.spinners import Spinners; Spinners.dots | Spinners.arc | Spinners.clock | Spinners.earth | Spinners.moon | Spinners.pong | Spinners.bouncingBar | Spinners.shark. Custom: sp.spinner = {"interval":100,"frames":["⠋","⠙","⠹","⠸","⠼"]}. Color choices: blue, green, red, yellow, cyan, magenta, white. on_color: on_blue, on_green, on_red, on_yellow. Attrs: bold, dark, underline, blink, reverse, concealed. Nested: combine with indent via custom prefix. TTY: sp = yaspin(); sp.enabled = sys.stdout.isatty(). Claude Code generates yaspin spinners for deploy scripts, import jobs, and long-running CLI tools.
CLAUDE.md for yaspin
## yaspin Stack
- Version: yaspin >= 3.0 | pip install yaspin
- Basic: with yaspin(text="Loading", color="cyan") as sp: do_work(); sp.ok("✔")
- Spinners: Spinners.dots | .earth | .moon | .arc | .pong | .shark — 100+ in Spinners enum
- Outcomes: sp.ok(text) | sp.fail(text) — mark done; writes final line
- Live print: sp.write("message") — prints line without breaking animation
- Timer: yaspin(timer=True) — appends elapsed time to spinner text
- Hidden: with sp.hidden(): print(...) — suppress spinner for clean output
yaspin Terminal Spinner Pipeline
# app/spinner.py — yaspin spinner utilities for CLI tools and long-running tasks
from __future__ import annotations
import signal
import subprocess
import sys
import time
from contextlib import contextmanager
from typing import Any, Callable, Iterator, TypeVar
from yaspin import yaspin
from yaspin.spinners import Spinners
T = TypeVar("T")
# ─────────────────────────────────────────────────────────────────────────────
# 1. Spinner factory
# ─────────────────────────────────────────────────────────────────────────────
def make_spinner(
text: str = "Working",
spinner=Spinners.dots,
color: str = "cyan",
timer: bool = False,
side: str = "left",
enabled: bool | None = None,
):
"""
Create a yaspin spinner.
enabled: None → auto-detect TTY; True/False → force on/off.
"""
sp = yaspin(spinner=spinner, text=text, color=color, timer=timer, side=side)
if enabled is not None:
sp.enabled = enabled
elif not sys.stderr.isatty():
sp.enabled = False
return sp
@contextmanager
def spin(
text: str = "Working",
spinner=Spinners.dots,
color: str = "cyan",
ok_text: str = "✔",
fail_text: str = "✗",
timer: bool = False,
) -> Iterator[Any]:
"""
Context manager: auto-calls sp.ok() on success, sp.fail() on exception.
Usage:
with spin("Fetching data") as sp:
data = requests.get(url).json()
sp.write(f" Got {len(data)} records")
# → prints "✔ Fetching data"
"""
sp = make_spinner(text, spinner=spinner, color=color, timer=timer)
sp.start()
try:
yield sp
sp.ok(ok_text)
except Exception as exc:
sp.fail(f"{fail_text} {exc}")
raise
# ─────────────────────────────────────────────────────────────────────────────
# 2. Step runner
# ─────────────────────────────────────────────────────────────────────────────
def run_steps(
steps: list[tuple[str, Callable[[], Any]]],
spinner=Spinners.dots,
color: str = "cyan",
stop_on_error: bool = True,
ok_symbol: str = "✔",
fail_symbol: str = "✗",
) -> list[dict[str, Any]]:
"""
Run labeled steps with a spinner each.
Returns list of {"step", "ok", "result"|"error"} dicts.
"""
results = []
for label, fn in steps:
sp = make_spinner(label, spinner=spinner, color=color)
sp.start()
try:
result = fn()
sp.ok(ok_symbol)
results.append({"step": label, "ok": True, "result": result})
except Exception as exc:
sp.fail(fail_symbol)
results.append({"step": label, "ok": False, "error": str(exc)})
if stop_on_error:
break
return results
def step(label: str, fn: Callable[[], T], spinner=Spinners.dots, color: str = "cyan") -> T:
"""Run a single labeled function with a spinner. Returns fn's return value."""
with spin(label, spinner=spinner, color=color) as sp:
result = fn()
return result
# ─────────────────────────────────────────────────────────────────────────────
# 3. Live-logging spinner
# ─────────────────────────────────────────────────────────────────────────────
class LoggingSpinner:
"""
Spinner that supports sp.log(msg) — prints lines while animation runs.
Usage:
with LoggingSpinner("Processing files") as sp:
for path in paths:
process(path)
sp.log(f" Processed: {path}")
"""
def __init__(
self,
text: str,
spinner=Spinners.dots,
color: str = "cyan",
timer: bool = True,
):
self._sp = make_spinner(text, spinner=spinner, color=color, timer=timer)
self._text = text
def __enter__(self) -> "LoggingSpinner":
self._sp.start()
return self
def log(self, message: str) -> None:
"""Print a line without interrupting the spinner."""
self._sp.write(message)
def update(self, text: str) -> None:
"""Update the spinner label."""
self._sp.text = text
def ok(self, text: str | None = None) -> None:
self._sp.ok(f"✔ {text or self._text}")
def fail(self, text: str | None = None) -> None:
self._sp.fail(f"✗ {text or self._text}")
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type:
self._sp.fail(f"✗ {self._text}")
else:
self._sp.ok(f"✔ {self._text}")
return False
# ─────────────────────────────────────────────────────────────────────────────
# 4. Subprocess helpers
# ─────────────────────────────────────────────────────────────────────────────
def run_command(
cmd: list[str],
text: str | None = None,
check: bool = True,
capture_output: bool = True,
spinner=Spinners.dots,
) -> subprocess.CompletedProcess:
"""Run a command with a yaspin spinner."""
label = text or " ".join(cmd)
sp = make_spinner(label, spinner=spinner)
sp.start()
try:
result = subprocess.run(cmd, check=check, capture_output=capture_output, text=True)
sp.ok("✔")
return result
except subprocess.CalledProcessError as exc:
sp.fail("✗")
raise
def run_pipeline(
commands: list[tuple[str, list[str]]],
stop_on_error: bool = True,
) -> bool:
"""Run a list of (label, command) pairs with spinners. Returns True if all pass."""
for label, cmd in commands:
sp = make_spinner(label)
sp.start()
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
sp.ok("✔")
except subprocess.CalledProcessError:
sp.fail("✗")
if stop_on_error:
return False
return True
# ─────────────────────────────────────────────────────────────────────────────
# 5. Timer spinner (shows elapsed)
# ─────────────────────────────────────────────────────────────────────────────
@contextmanager
def timed_spin(text: str, spinner=Spinners.arc, color: str = "yellow") -> Iterator[Any]:
"""
Spinner with elapsed time display.
Uses yaspin(timer=True) — appends elapsed seconds automatically.
"""
sp = make_spinner(text, spinner=spinner, color=color, timer=True)
sp.start()
try:
yield sp
sp.ok("✔")
except Exception as exc:
sp.fail(f"✗ {exc}")
raise
# ─────────────────────────────────────────────────────────────────────────────
# 6. Signal-safe spinner
# ─────────────────────────────────────────────────────────────────────────────
@contextmanager
def interruptible_spin(text: str) -> Iterator[Any]:
"""
Spinner that handles SIGTERM/SIGINT gracefully.
Calls sp.fail() with a message when interrupted.
"""
sp = make_spinner(text)
def _handle_sigterm(signum, frame):
sp.fail("✗ Terminated")
sys.exit(1)
old_sigterm = signal.getsignal(signal.SIGTERM)
signal.signal(signal.SIGTERM, _handle_sigterm)
sp.start()
try:
yield sp
sp.ok("✔")
except KeyboardInterrupt:
sp.fail("✗ Interrupted")
raise
finally:
signal.signal(signal.SIGTERM, old_sigterm)
# ─────────────────────────────────────────────────────────────────────────────
# 7. Spinner catalog
# ─────────────────────────────────────────────────────────────────────────────
SPINNER_CATALOG: dict[str, Any] = {
"dots": Spinners.dots,
"arc": Spinners.arc,
"clock": Spinners.clock,
"earth": Spinners.earth,
"moon": Spinners.moon,
"pong": Spinners.pong,
"bounce": Spinners.bouncingBall,
"line": Spinners.line,
"shark": Spinners.shark,
"hearts": Spinners.hearts,
}
def demo_styles(duration: float = 0.6) -> None:
"""Demo each spinner style briefly."""
for name, style in SPINNER_CATALOG.items():
sp = make_spinner(f"Style: {name}", spinner=style)
sp.start()
time.sleep(duration)
sp.ok("✔")
# ─────────────────────────────────────────────────────────────────────────────
# 8. Loguru integration
# ─────────────────────────────────────────────────────────────────────────────
def with_loguru_spinner(text: str, spinner=Spinners.dots):
"""
Decorator: wraps a function with a yaspin spinner, routing logger output
through sp.write() so log lines don't break the animation.
Usage:
@with_loguru_spinner("Importing data")
def import_data(path):
logger.info("Reading {}", path)
...
"""
try:
from loguru import logger as _logger
except ImportError:
_logger = None
import functools
def decorator(fn: Callable) -> Callable:
@functools.wraps(fn)
def wrapper(*args, **kwargs):
sp = make_spinner(text, spinner=spinner)
sp.start()
try:
if _logger:
handler_id = _logger.add(lambda msg: sp.write(msg.rstrip()))
result = fn(*args, **kwargs)
sp.ok("✔")
return result
except Exception as exc:
sp.fail(f"✗ {exc}")
raise
finally:
if _logger and handler_id is not None:
_logger.remove(handler_id)
return wrapper
return decorator
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Basic spin context manager ===")
with spin("Loading configuration", ok_text="✔") as sp:
time.sleep(0.4)
sp.write(" Config: env=production")
print("\n=== Timed spinner ===")
with timed_spin("Running database migration") as sp:
time.sleep(0.5)
print("\n=== Logging spinner ===")
with LoggingSpinner("Processing records", timer=True) as sp:
for i in range(3):
time.sleep(0.2)
sp.log(f" Record {i+1} processed")
sp.update("Finalising...")
time.sleep(0.2)
print("\n=== Step runner ===")
results = run_steps([
("Validating input", lambda: time.sleep(0.2)),
("Fetching upstream", lambda: time.sleep(0.3)),
("Writing output", lambda: time.sleep(0.2)),
])
for r in results:
print(f" {r['step']}: {'OK' if r['ok'] else 'FAIL'}")
print("\n=== Spinner styles ===")
for name, style in list(SPINNER_CATALOG.items())[:4]:
sp = make_spinner(f"Style: {name}", spinner=style)
sp.start()
time.sleep(0.4)
sp.ok("✔")
print("\n=== Side=right spinner ===")
sp = make_spinner("Loading", spinner=Spinners.arc, side="right")
sp.start()
time.sleep(0.4)
sp.ok("✔")
For the halo alternative — Halo outputs to stderr by default whereas yaspin defaults to stdout and targets stderr only when explicitly configured; yaspin exposes yaspin(timer=True) for elapsed time display that Halo lacks, and yaspin’s spinners.Spinners enum gives IDE auto-completion across 100+ styles; both are solid choices with very similar APIs, choose Halo when you prefer its slightly simpler sp.succeed()/sp.fail()/sp.warn()/sp.info() outcome methods (yaspin only has ok()/fail()). For the rich.status alternative — rich.Status renders with rich markup, supports live-updating layouts with rich.live.Live, and integrates with Tables and Panels in the same render cycle; yaspin is a standalone 30 KB library with zero heavy dependencies, which is preferable when you don’t need the full rich ecosystem and want a spinner that works reliably in any POSIX terminal. The Claude Skills 360 bundle includes yaspin skill sets covering yaspin() constructor with spinner/color/timer/side, ok()/fail() outcomes, sp.write() non-breaking log lines, Spinners enum catalog, make_spinner() factory, spin() context manager, timed_spin() elapsed timer, LoggingSpinner with live logging, run_steps() pipeline, run_command()/run_pipeline() subprocess wrappers, interruptible_spin() SIGTERM handler, and with_loguru_spinner() decorator. Start with the free tier to try terminal spinner animation code generation.