# Diagnose and fix astropy Quantity and unit errors
Use this when arithmetic with `astropy.units` raises `UnitConversionError` or
`TypeError`, or when units silently vanish and downstream numbers come out
wrong by orders of magnitude. Nearly every unit bug is one of four failure
modes, and this procedure identifies which one from the exception (or its
absence) before you touch the code.
## 1. Read the exception class — it names the failure mode
- `UnitConversionError: 'm' (length) and 's' (time) are not convertible` —
the two operands have genuinely incompatible physical types, or they are
convertible only through an equivalency you have not enabled (section 2).
- `TypeError: only dimensionless scalar quantities can be converted to
Python scalars` — you called `float()`, `int()`, or `round()` on a
quantity that still carries a physical unit (section 3).
- No exception, but later code misbehaves and values look like bare numbers
— the units were silently dropped, almost always by a numpy function that
was not told to preserve subclasses (section 4).
Check the physical types of both sides first — it is the fastest diagnostic:
```python
from astropy import units as u
q = 500 * u.nm
print(q.physical_type) # 'length'
print(q.is_equivalent(u.Hz)) # False -- needs an equivalency
print((10 * u.m).is_equivalent(u.km)) # True -- plain conversion, no equivalency needed
```
## 2. UnitConversionError: incompatible types vs missing equivalency
`UnitConversionError` from `.to()` or from `+`/`-` means one of two things.
Distinguish them with `is_equivalent()`:
- `is_equivalent(target)` is `True` — the conversion is legal but something
else is wrong (usually a plain number was passed where a `Quantity` was
expected). Convert explicitly with `.to()`.
- `is_equivalent(target)` is `False` — either the conversion is genuinely
meaningless (length to time: your formula is wrong, fix the math), or it
needs an **equivalency**: a context-dependent mapping between physical
types.
The equivalencies you will actually reach for:
```python
freq = (500 * u.nm).to(u.Hz, equivalencies=u.spectral()) # wavelength <-> frequency <-> energy
```
- `u.spectral()` — wavelength, frequency, energy. It does *not* cover
Doppler shifts; that is a separate equivalency, a common confusion.
- `u.doppler_optical(rest_wavelength)` / `u.doppler_radio(...)` —
wavelength/frequency <-> velocity, optical and radio conventions.
- `u.spectral_density(ref_wavelength)` — flux density <-> surface
brightness, needs a reference wavelength.
- `u.dimensionless_angles()` — angle <-> dimensionless (radians to 1).
- `u.mass_energy()`, `u.brightness_temperature()` — niche but real.
Scope the equivalency to the block that needs it rather than enabling it
globally:
```python
with u.set_enabled_equivalencies(u.dimensionless_angles()):
x = (0.5 * u.rad).to(u.dimensionless_unscaled) # 0.5
```
## 3. TypeError on float()/int(): convert to a plain number deliberately
`float(3 * u.m)` always fails. The fix is to say which unit the number is
in, explicitly:
```python
float((3.2 * u.km).to(u.m)) # 3200.0
(3.2 * u.km).to_value(u.m) # 3200.0 -- plain float, no intermediate Quantity
```
`to_value(unit)` is the right API when you want a number out; `.value`
alone returns the number in whatever unit the quantity currently carries,
which is often not the unit you assumed.
Dimensionless quantities are the exception — `float()` works and the scale
is folded in (`float(3. * u.km / (4. * u.m))` is `750.0`). But watch the
subtlety: a "dimensionless" quantity like `1. * u.m / u.km` has unit `m/km`
with a scale of 0.001. Adding it to a plain float auto-decomposes to
scale-free (`1. + 1. * u.m / u.km` gives `1.001`), while `.value` gives the
raw `1.0`. If you want the scaled number, call `.decompose()` first or
convert explicitly.
## 4. Units silently dropped: the numpy boundary
`Quantity` is an ndarray subclass, and several numpy entry points return a
bare ndarray unless asked to preserve the subclass:
```python
q = np.arange(10.) * u.m
np.array(q) # bare array -- units GONE (documented)
np.array(q, subok=True) # keeps the Quantity, units of m
np.broadcast_to(q, (2, 10)) # bare array -- units GONE
np.broadcast_to(q, (2, 10), subok=True) # keeps units
```
Same class of bug: calling `.value` "just to be safe" and then continuing
in unit-less code, or feeding a quantity to a library function that
internally calls `np.asarray` without `subok=True`. When a downstream
result is mysteriously off by orders of magnitude, check `type(result)` and
`result.unit` at the numpy boundary before doubting the physics.
For validating function arguments, `u.quantity_input` checks that inputs
are quantities with units convertible to the declared one — it checks, it
does *not* convert, so `myfunction(100 * u.arcsec)` still arrives as
arcseconds when the declared unit is degrees.
## 5. Checklist
1. Read the exception class: `UnitConversionError` / scalar-conversion
`TypeError` / silent (no exception).
2. `UnitConversionError` → run `is_equivalent()` on both sides. `True`:
wrong operand shape, convert explicitly. `False`: fix the formula or
add an equivalency.
3. Equivalency needed → pass `equivalencies=` to `.to()`, or
`set_enabled_equivalencies` for a block. `spectral()` does not cover
Doppler.
4. `TypeError` on `float`/`int` → `.to(unit).value` or `.to_value(unit)`;
never `float(q)` directly on a unit-carrying quantity.
5. Silent loss → grep the numpy boundary for `np.array(`, `np.broadcast_to`,
`np.asarray` without `subok=True`; check `.value` is not taken too early.