An MLX array is a typed N-dimensional buffer in unified memory. You make one from a Python list (or a NumPy array, or another MLX array), it has a shape, a dtype, and an ndim. If you've used NumPy or PyTorch tensors, the next 30 seconds of typing will feel exactly the same.
What you'll notice as different is small but architectural — there's no device argument anywhere, and there will not be one in the next lesson, or the lesson after that.
The dtype set is ML-shaped, not science-wide
NumPy supports a wide set of numeric types because it's a general-purpose scientific library — complex128, datetime64, structured records, the works. MLX's dtype set is shaped for ML workloads. As of mlx 0.31.x, the supported dtypes are float32 (the default for most things), float16, bfloat16, int8 / int16 / int32 / int64, uint8 / uint16 / uint32 / uint64, bool, and a couple of complex types. No datetime, no structured records, no extended precision.
That's a feature, not a limitation. The smaller surface means simpler kernel code in MLX, which means fewer paths to optimize and fewer corner cases to maintain. If you need complex128, you're probably reaching for the wrong tool.
Default device is unified memory, always
Every mx.array you create lives in unified memory by default. mx.default_device() returns Device(gpu, 0) on Apple Silicon — but as you learned in foundations.lesson2, that's a label about which compute unit will run the next op, not where the bytes live. The bytes live in one shared pool the whole time.
Type promotion is the one place to be careful
If you mix dtypes in an op (e.g. int32_array + float32_array), MLX promotes to the wider type using rules that match NumPy in most cases but not all. The safe habit, especially when you're mixing learned weights (always float) with integer indices, is to call .astype(mx.float32) explicitly when you mean to. Implicit promotion is convenient until it loses you a debugging hour.
Code
Building arrays — the same shapes you know from NumPy·python
import mlx.core as mx
# The default 'device' is just a label about where compute runs next.
# All bytes live in the unified-memory pool either way.
print('default device:', mx.default_device()) # → Device(gpu, 0) on Apple Silicon
x = mx.array([1.0, 2.0, 3.0])
print('x lives in :', mx.default_device(), '— but the bytes are in unified memory.')
Build five arrays of your own choosing — one int32, one float32, one float16, one bool, and one shape (3, 5) of zeros. For each, print the dtype, shape, ndim, and size. Then take any two of them and do an arithmetic op (+ or *) — what dtype does the result have? Try one combination that promotes to a higher-precision dtype and one that doesn't. Two sentences on what you noticed.
Progress
Progress is local-only — sign in to sync across devices.