Short: They have different scopes: the config sizes the engine's own Exchanges, repartition(1000) sizes that one explicit shuffle.
Detailed: Every groupBy or sort-merge join Exchange targets spark.sql.shuffle.partitions unless AQE coalesces it at runtime; repartition(1000) is its own Exchange with its own target, and a later groupBy re-shuffles back to the configured number. df.write.partitionBy is a third, unrelated thing — it controls the on-disk directory layout, not in-memory partition count.
Senior: repartition on the same columns you pass to write.partitionBy, so each task owns one output directory and writes one decent-sized file instead of every task dropping a fragment into every directory.
Common mistake: Conflating write.partitionBy with repartition and expecting one to control the other.
Follow-up: How do you get a sane directory layout and reasonable file sizes at the same time?
Lesson · Simulation