Three arguments control how a Matplotlib scatter marker looks, and each one does a single job:
plt.scatter(x, y,
marker="^", # the SHAPE
s=200, # the SIZE, as an area in points squared
c="#c0392b") # the COLOUR
s is the one that surprises people. It is an area, so s=400 is twice as wide as s=100, not four times.
Every chart below is a real Matplotlib window from Python 3.12.5, Matplotlib 3.11.2, NumPy 2.5.3.
import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5]
y = [2, 4, 3, 5, 4]
plt.figure(figsize=(7, 4))
plt.scatter(x, y, marker="^", s=200, c="#c0392b") # shape, size, colour
plt.title("marker, s and c control everything")
plt.grid(alpha=.3); plt.tight_layout()
plt.show()

marker, s and c.Every Matplotlib marker style and its code
Matplotlib ships 37 marker codes. Most guides list five of them, so here is the whole set drawn out:
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
markers = [(k, v) for k, v in Line2D.markers.items() if v != "nothing"]
print("markers available:", len(markers))
fig, ax = plt.subplots(figsize=(10, 5))
for i, (code, name) in enumerate(markers):
row, col = divmod(i, 8)
ax.scatter(col, -row, marker=code, s=220, c="#0b6bcb")
ax.text(col, -row - 0.32, repr(code), ha="center", fontsize=8, color="#333")
ax.set_title("Every Matplotlib marker and its code")
ax.set_xlim(-0.6, 7.6); ax.set_ylim(-4.7, 0.6)
ax.axis("off")
plt.tight_layout(); plt.show()
Output:
markers available: 37
C:\pyguides\runs\scattermarker\ex_allmarkers.py:31: UserWarning: The pixel maker ',' is not supported on scatter(); using a finite-sized square instead, which is not necessarily 1 pixel in size. Use the square marker 's' instead to suppress this warning.
ax.scatter(col, -row, marker=code, s=220, c="#0b6bcb")

marker=.The codes worth memorising are the obvious ones: "o" circle, "s" square, "^" triangle, "*" star, "D" diamond and "x" cross.
- Filled markers —
o s ^ v < > p * h H D d P X 8 .take a face colour and an edge colour. - Unfilled markers —
+ x 1 2 3 4 | _are drawn as lines, soedgecolorsdoes nothing to them. marker="$Z$"renders any character as the marker, which is handy for one-off labels.
The default is "o". You can change it globally with plt.rcParams["scatter.marker"] if you’d rather not repeat yourself.
Matplotlib scatter marker size: the s parameter
s is an area measured in points squared. That sounds pedantic until a marker comes out the wrong size:
import numpy as np
from matplotlib.lines import Line2D
# s is an AREA in points squared, so the diameter is its square root
for s in (25, 100, 400, 900):
print(f"s={s:<5} -> diameter {np.sqrt(s):>5.1f} points")
print()
print("Doubling the width means multiplying s by 4, not by 2.")
print()
print("filled markers:", Line2D.filled_markers)
print("unfilled ones ('1','2','+','x') ignore facecolor and only take a colour")
Output:
s=25 -> diameter 5.0 points
s=100 -> diameter 10.0 points
s=400 -> diameter 20.0 points
s=900 -> diameter 30.0 points
Doubling the width means multiplying s by 4, not by 2.
filled markers: ('.', 'o', 'v', '^', '<', '>', '8', 's', 'p', '*', 'h', 'H', 'D', 'd', 'P', 'X')
unfilled ones ('1','2','+','x') ignore facecolor and only take a colour

s=100 gives a 10-point marker, s=400 a 20-point one.The default is s=36, which is a 6-point marker. If you want markers twice as wide, multiply s by four.
s also takes one value per point, and that is how you build a bubble chart:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(0)
x, y = rng.random(40), rng.random(40)
population = rng.integers(10, 400, 40)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.2))
for i, s in enumerate((25, 100, 400, 900)):
ax1.scatter(i, 0, s=s, c="#0b6bcb")
ax1.text(i, -0.55, f"s={s}", ha="center", fontsize=9)
ax1.set_title("One size per call")
ax1.set_ylim(-1, 1); ax1.axis("off")
# s also accepts one value per point, which is how bubble charts are made
ax2.scatter(x, y, s=population, c="#0a7d32", alpha=.6, edgecolors="white")
ax2.set_title("One size per point (a bubble chart)")
ax2.grid(alpha=.3)
plt.tight_layout(); plt.show()

s on the left, an array of sizes on the right.Because the eye reads bubble area rather than radius, mapping a quantity straight to s is honest. Scaling the radius instead exaggerates large values. The same care applies when you’re fitting a line through a scatter plot.
Matplotlib scatter color for a single colour or a category
One colour for every point is just c= with a colour name or hex code. Categories need one call each:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(1)
groups = {"setosa": "#0b6bcb", "versicolor": "#c0392b", "virginica": "#0a7d32"}
plt.figure(figsize=(7.5, 4.5))
for i, (name, colour) in enumerate(groups.items()):
x = rng.normal(i * 2, .5, 30)
y = rng.normal(i, .5, 30)
plt.scatter(x, y, c=colour, label=name, s=70, alpha=.8)
plt.legend(title="species")
plt.title("A separate scatter call per category")
plt.grid(alpha=.3); plt.tight_layout()
plt.show()

Calling scatter once per group is what makes the legend work. A single call with mixed colours gives you one legend entry, not three.
If the legend needs more control than label= gives you, see the scatter plot legend guide.
Matplotlib scatter color by value with a colormap
When the colour should represent a number rather than a group, pass the numbers themselves to c:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(2)
x, y = rng.random(200), rng.random(200)
depth = x * 2 + y # the value we want colour to represent
plt.figure(figsize=(7.5, 4.8))
sc = plt.scatter(x, y, c=depth, cmap="viridis", s=60)
plt.colorbar(sc, label="depth")
plt.title("c= a numeric array, mapped through a colormap")
plt.tight_layout(); plt.show()
print("c received", depth.shape[0], "numbers, not colours")
print("colormap used:", sc.get_cmap().name)
print("value range mapped:", round(float(depth.min()), 3), "to", round(float(depth.max()), 3))
Output:
c received 200 numbers, not colours
colormap used: viridis
value range mapped: 0.135 to 2.866

c=depth with cmap="viridis", and a colorbar to read it.Matplotlib scales your numbers to the 0–1 range and looks each one up in the colormap. The default is viridis.
Always add the colorbar. Without it the colours are decoration, because nobody can tell which shade means which value.
Why does color= raise a ValueError in scatter?
Because c and color are not the same argument, and only one of them maps numbers:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
values = [0.1, 0.5, 0.9]
# c= maps numbers through a colormap
sc = ax.scatter([1, 2, 3], [1, 2, 3], c=values)
print("c= numbers -> OK, mapped through", sc.get_cmap().name)
# color= refuses them
try:
ax.scatter([1, 2, 3], [1, 2, 3], color=values)
except ValueError as err:
print("color= numbers ->", err)
print()
print("c= also accepts real colours:", ["red", "green", "blue"])
ax.scatter([1, 2, 3], [3, 2, 1], c=["red", "green", "blue"])
print("that works too, which is why the two arguments get confused")
Output:
c= numbers -> OK, mapped through viridis
c= also accepts real colours: ['red', 'green', 'blue']
that works too, which is why the two arguments get confused

c.The message reads 'color' kwarg must be a color or sequence of color specs, and it ends by telling you to use c instead.
| Argument | Accepts | Colormapped? |
|---|---|---|
c= | Colours or numbers | Yes, when given numbers |
color= | Colours only | No |
cmap= | A colormap name | Only affects c as numbers |
Use c by default in scatter. It do