In PyTorch, how does type promotion pick the result dtype of a mixed-dtype op?
answer
- category first, then width
- bool, integer, float, complex
- Python scalars are weak participants
- integer division still yields a float
- result_type answers it without running
basics
~20 sPyTorch picks the dtype that can represent both operands: the higher category first (bool below integer below float below complex), then the wider type within that category. Python scalars and 0-dim tensors count only weakly and never widen a dimensioned operand.
solid answer
~40 sPromotion runs in two steps. First the **category** — bool < integer < floating point < complex — is raised to the higher of the two, which is why dividing two integer tensors yields a floating tensor at `torch.get_default_dtype()` (float32). Then, within a category, the **wider** type wins, so float32 combined with float64 gives float64. The subtlety is participation: dimensioned tensors dominate. A Python scalar or a 0-dim tensor can raise the category but cannot widen the type, so `float32_tensor * torch.tensor(2.0, dtype=torch.float64)` stays float32 while the same op against a `(3,)` float64 tensor gives float64. Defaults matter too: `torch.tensor([1, 2, 3])` is int64, `torch.tensor([1.0])` is float32 (not float64 as in NumPy), and arrays arriving via `torch.from_numpy` are usually float64. `torch.result_type(a, b)` answers the question without running the op.
code
python · 11 linesimport torch
print(torch.tensor([1, 2, 3]).dtype) # torch.int64
print(torch.tensor([1.0, 2.0]).dtype) # torch.float32
print((torch.tensor([1, 2, 3]) / 2).dtype) # torch.float32
f32 = torch.ones(3, dtype=torch.float32)
weak = torch.tensor(2.0, dtype=torch.float64) # 0-dim
strong = torch.ones(3, dtype=torch.float64) # 1-dim
print((f32 * weak).dtype) # torch.float32
print(torch.result_type(f32, strong)) # torch.float64go deeper
Remember the defaults: integer literals give int64, float literals give float32, and dividing two integer tensors gives a float tensor. Know that .float() and .to(dtype) are how you convert.
Explain the two-step rule — category first, then width — and the weak participation of Python scalars and 0-dim tensors. Mention torch.result_type and why in-place ops refuse to promote.
Talk about the cost side: a float64 that promotes successfully is worse than one that errors, because it silently doubles memory and slows GPU math, and about pinning dtype at ingest with boundary assertions.
Set the numeric policy for the codebase — the storage dtype for data, the compute dtype for models, where conversions are allowed to happen — so precision is a deliberate decision rather than an emergent property of whichever library produced the array.
## Why this exists PyTorch did not always promote types; early versions simply refused mixed-dtype arithmetic. Modern PyTorch follows a NumPy-like promotion scheme so that mixing dtypes usually just works, which is convenient and occasionally expensive — a stray float64 can double a tensor's memory and cut throughput, particularly on GPUs where float64 is drastically slower than float32. ## The rules Each dtype has a **category**, ordered bool < integer (including unsigned) < floating point < complex, and a width within the category. Given two operands: 1. Take the higher category of the two. 2. Within that category, take the type wide enough to hold both. 3. Apply participation rules: operands that are Python numbers or 0-dim tensors — internally "wrapped numbers" — can bump the category but do not influence the width. Dimensioned tensors decide the width. So `torch.ones(3, dtype=torch.int64) * 2.5` gives float32: the Python float raises the category to floating point, and since no dimensioned floating operand exists the default dtype supplies the width. But `torch.ones(3, dtype=torch.float32) * torch.tensor(2.0, dtype=torch.float64)` stays float32, because the float64 operand is 0-dim and therefore weak. Two dimensioned floats of different widths, `float32` and `float64`, give float64. Division is special-cased at the category level: `/` on integers is true division, so `torch.tensor([1, 2, 3]) / 2` is float32 rather than integer. Use `torch.div(..., rounding_mode="floor")` or `//` when you want integer division. Comparisons return `torch.bool` regardless of operand types. ## The defaults you have to remember - `torch.tensor([1, 2, 3])` → `torch.int64`. - `torch.tensor([1.0, 2.0])` → `torch.float32`, even though Python floats are 64-bit. This is the single biggest difference from NumPy, whose default is float64. - `torch.get_default_dtype()` returns float32; `torch.set_default_dtype()` changes it globally, which is a blunt instrument best avoided in library code. - `torch.zeros`, `torch.ones`, `torch.rand`, `torch.randn` and friends use the default dtype unless you pass `dtype=`. - `torch.from_numpy` preserves the array's dtype, so float64 arrays produce float64 tensors — the usual way float64 sneaks into a float32 pipeline. ## In-place ops do not promote An underscore op has to write into the destination's existing storage, so it cannot change the destination's dtype. `int_tensor.add_(2.5)` raises rather than promoting to float, with an error about the result type not being castable to the output. This is a useful accidental type check: if in-place arithmetic starts failing, something upstream changed a dtype. The same applies to `out=` arguments, which must already have a castable dtype. ## Where wrong dtypes hurt A float64 tensor entering a float32 model raises a dtype-mismatch RuntimeError at the first matmul — loud, and easy to fix. The quieter damage is when everything promotes successfully: a float64 branch inside a preprocessing pipeline doubles memory and, on GPU, runs an order of magnitude slower than float32 on consumer hardware. In the other direction, integer overflow is silent — int8 and int32 accumulators wrap around without warning, so counting or summing large integer tensors should be done in int64 or a float type. Index tensors are their own trap: indexing and `gather`/`scatter` want int64 (`torch.long`) index tensors, and an int32 index tensor raises. Loss functions such as cross-entropy want int64 class targets, not float. ## Tools for reasoning about it `torch.result_type(a, b)` reports the promoted dtype without executing the op — good for assertions and for writing library code that must allocate an output. `torch.promote_types(dtype1, dtype2)` answers the same question for dtypes alone. `torch.can_cast(from, to)` reports whether a cast is allowed under the promotion rules. To convert, use `Tensor.to(dtype)` (or the shortcuts `.float()`, `.double()`, `.long()`, `.half()`), remembering that `.to()` returns `self` unchanged when the dtype already matches, so it is cheap to call defensively. ## The habit Pin the dtype at ingest — as the data enters the pipeline, not layer by layer — and assert it at component boundaries. Most dtype incidents are a single unconverted NumPy array or an integer label tensor that a helper turned into a float, and both are caught by one assertion at the edge.
- Why does torch.tensor([1.0]) give float32 when the Python literal is a 64-bit float?Because PyTorch's default dtype is float32, as `torch.get_default_dtype()` reports, and factory functions use it rather than mirroring Python's precision. Deep learning runs in float32 or lower, so float64 would be a poor default. This differs from NumPy, which defaults to float64 — the mismatch is why arrays crossing over with `from_numpy` so often arrive as float64 tensors.
- Why does int_tensor.add_(2.5) raise instead of promoting to float?In-place ops write into the destination's existing storage, which has a fixed dtype, so there is nowhere to put a promoted result; PyTorch raises a result-type error rather than silently truncating. The same restriction applies to `out=` arguments. It is a useful signal: an in-place op that suddenly fails usually means an upstream dtype changed.
- How do you check the promoted dtype of an operation without running it?`torch.result_type(a, b)` returns the dtype the operation would produce, accepting tensors or Python numbers; `torch.promote_types(d1, d2)` does the same for dtypes alone, and `torch.can_cast(from, to)` reports whether a cast is permitted. These are useful in library code that needs to preallocate an output buffer, and as assertions at pipeline boundaries.
- Which dtype do index tensors and classification targets need?int64. Indexing, `gather` and `scatter` require int64 index tensors and raise on int32, and cross-entropy-style losses expect int64 class indices rather than floats. Since `torch.tensor([0, 1, 2])` already gives int64 this is usually automatic; it breaks when indices arrive from NumPy as int32 on Windows or get cast to float by a generic helper.
saying these in an interview costs you the question
- Assumes torch.tensor([1.0]) is float64 like NumPy
- Thinks integer division returns an integer tensor
- Believes a 0-dim float64 tensor upcasts a float32 tensor
- Expects in-place ops to promote the destination dtype
- Ignores float64 creeping in because the math still runs