## TL;DR
markupsafe 2.1 deleted `soft_unicode`, but jinja2 versions before 3.1 still try to import it, so any environment with new markupsafe and old jinja2 breaks the moment Jupyter renders a template. Fix it with one command: `pip install -U jinja2 markupsafe`. Then verify with `python -c "import jinja2"` before restarting the notebook server.

```text
ImportError: cannot import name 'soft_unicode' from 'markupsafe'
```

## Use this when
- Starting Jupyter or rendering any template fails with this exact ImportError.
- `pip show markupsafe` says 2.1+ while `pip show jinja2` says below 3.1.
- The traceback passes through jinja2's `utils.py` or `filters.py`.

## Not for this skill when
- The error names a different missing name from markupsafe. Check which jinja2 version expects it.
- Templates render but look wrong. That is a template-version issue, not this import break.

## Steps
1. Confirm the skew: `pip show markupsafe jinja2 | grep -E "Name|Version"`. Verify: markupsafe 2.1 or newer with jinja2 below 3.1.
2. Upgrade both together: `pip install -U jinja2 markupsafe`. Verify with `python -c "import jinja2; print(jinja2.__version__)"` that jinja2 reports 3.1 or newer.
3. If a pinned requirements file forces old jinja2, update the pin instead of fighting it. Verify: the lockfile has jinja2 3.1 or newer.
4. Restart the Jupyter server completely (not just the kernel). Verify: the server starts and the tree page renders.
5. Run a template-heavy page like the notebook dashboard. Verify: no ImportError in the server log.

## Variant phrasings
### soft_unicode markupsafe jupyter
Short form. The paired upgrade is the fix.

### jinja2 markupsafe incompatibility jupyter
Names the pair. Same one-command fix.

### ImportError soft_unicode after upgrade
Upgrade-context phrasing. Partial upgrades cause the skew.

Compatibility: jinja2 3.1+ with markupsafe 2.1+; notebook 6/7, JupyterLab 3/4. Upgrading the pair is safe for all of them.

## Why it happens
markupsafe 2.1 removed long-deprecated helpers including `soft_unicode`, assuming consumers had moved on. jinja2 only stopped importing it in 3.1. Any resolver that upgraded markupsafe without touching jinja2 produced an environment where jinja2's own import line fails, and since Jupyter renders everything through jinja2 templates, the whole server breaks on startup.

## Edge cases / pitfalls
- Upgrading only markupsafe makes it worse. The pair must move together.
- Conda sometimes holds jinja2 back for an old nbconvert pin. Check what is constraining it before forcing.
- The server caches compiled templates; a stale cache can keep erroring after the fix. Restart the server process fully.
- If you vendor jinja2 inside another package, the vendored copy has its own markupsafe expectation. Update the vendor.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_b8ngtktrCzuHwLJ5IrAjoA
