StackPractices
beginner By Mathias Paulenko

Sort an Array

How to sort arrays and lists in ascending, descending, and custom order across multiple languages.

Topics: data

Overview

Sorting is one of the most common data manipulation tasks. Every language provides built-in, optimized sorting utilities. The pattern below demonstrates how to sort arrays and lists in ascending order, descending order, and by custom criteria (e.g., by a property or with a custom comparator).

When to Use

Use this recipe when:

  • Displaying data in a specific order (alphabetical, chronological, by priority). See Date Formatting for chronological sorting.
  • Preparing data for algorithms that require sorted input (binary search, merge)
  • Normalizing data before comparison or deduplication
  • Implementing ranking, leaderboards, or search result ordering. See Pagination for managing ordered results.

Solution

Python

numbers = [3, 1, 4, 1, 5, 9, 2, 6]

# Ascending (default)
asc = sorted(numbers)
# [1, 1, 2, 3, 4, 5, 6, 9]

# Descending
 desc = sorted(numbers, reverse=True)
# [9, 6, 5, 4, 3, 2, 1, 1]

# Sort objects by key
users = [
    {"name": "Bob", "age": 30},
    {"name": "Ada", "age": 36},
    {"name": "Chen", "age": 25},
]
by_age = sorted(users, key=lambda u: u["age"])
# Chen (25), Bob (30), Ada (36)

# Sort in-place
numbers.sort()

JavaScript

const numbers = [3, 1, 4, 1, 5, 9, 2, 6];

// Ascending
const asc = numbers.toSorted((a, b) => a - b);
// [1, 1, 2, 3, 4, 5, 6, 9]

// Descending
const desc = numbers.toSorted((a, b) => b - a);
// [9, 6, 5, 4, 3, 2, 1, 1]

// Sort objects by property
const users = [
  { name: 'Bob', age: 30 },
  { name: 'Ada', age: 36 },
  { name: 'Chen', age: 25 },
];
const byAge = users.toSorted((a, b) => a.age - b.age);
// Chen (25), Bob (30), Ada (36)

// In-place sort
numbers.sort((a, b) => a - b);

Java

import java.util.*;

List<Integer> numbers = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6));

// Ascending
Collections.sort(numbers);
// [1, 1, 2, 3, 4, 5, 6, 9]

// Descending
numbers.sort(Collections.reverseOrder());
// [9, 6, 5, 4, 3, 2, 1, 1]

// Sort objects by field
record User(String name, int age) {}
List<User> users = List.of(
    new User("Bob", 30),
    new User("Ada", 36),
    new User("Chen", 25)
);
List<User> byAge = users.stream()
    .sorted(Comparator.comparingInt(User::age))
    .toList();
// Chen (25), Bob (30), Ada (36)

// Custom comparator (name length descending)
users.stream()
    .sorted(Comparator.comparingInt((User u) -> u.name().length()).reversed())
    .toList();

Explanation

  • Stability: Python and JavaScript use Timsort, which is stable (equal elements keep their original order). Java’s Collections. sort() also uses Timsort and is stable.
  • Comparator contract: a comparator returns a negative number if a < b, zero if equal, and positive if a > b. Violating this contract (e. g. , inconsistent results) causes undefined behavior.
  • In-place vs. copy: list. sort() and Arrays. sort() modify the original; sorted() and toSorted() return a new collection. Prefer immutable copies unless memory is constrained.
  • Time complexity: built-in sorts are O(n log n) average and worst case. For specialized data (integers in a small range), counting sort can be O(n).

Variants

TaskPythonJavaScriptJava
Ascendingsorted(lst)toSorted((a,b)=>a-b)Collections.sort(list)
Descendingsorted(lst, reverse=True)toSorted((a,b)=>b-a)sort(reverseOrder())
By key/propertysorted(lst, key=fn)toSorted((a,b)=>a.p-b.p)sorted(Comparator.comparing(...))
In-placelst.sort()lst.sort(...)list.sort(...)

What Works

  • Use built-in sorts: do not implement your own sorting algorithm unless you have a very specific performance profile (e. g. , nearly-sorted data).
  • Keep comparators pure: comparator functions should not mutate data or depend on external state.
  • Handle ties explicitly: if two items are equal on the primary key, sort by a secondary key to ensure deterministic order.
  • Prefer immutability: returning a new sorted array/list avoids surprising side effects in the calling code.
  • Locale-aware sorting: for user-facing strings, use locale-sensitive collation (localeCompare in JS, locale. strxfrm in Python) rather than raw code-point comparison.

Common Mistakes

  • Sorting numbers alphabetically in JavaScript: [10, 2]. sort() produces [10, 2] because default sort converts elements to strings. Always pass a comparator for numbers.
  • Mutating during sort: modifying the array being sorted (e. g. , in a comparator with side effects) causes unpredictable results.
  • Inconsistent comparator: returning only 1 and -1 without 0 for equality can cause crashes or wrong results in some implementations.
  • Sorting huge datasets in memory: for datasets larger than available RAM, use external sorting or database ORDER BY. See Database Transactions for data consistency.
  • Assuming all sorts are stable: while most modern languages use stable sorts, do not rely on stability unless documented. Explicitly sort by secondary keys when order matters.

When Not to Use This Approach

  • Schema is unknown or frequently changing: if the data structure changes weekly, rigid validation schemas become a maintenance burden.
  • Data fits in a database: if the data needs querying, indexing, or transactions, storing it in JSON files and manipulating in-memory is the wrong approach.
  • Real-time validation of streaming data: batch validation of JSON payloads is too slow for streaming.
  • Simple type checking: if you only need to verify a value is a string or number, a full schema validator is overkill.
  • CPU-bound transformations on large datasets: if processing 10M+ records takes minutes, in-memory manipulation hits limits.
  • Distributed data processing: if data spans multiple machines, local JSON manipulation does not work.

Performance Benchmarks

  • JSON serialization: json. dumps() in Python serializes 1MB of data in 30-100ms. orjson serializes the same data in 5-15ms.
  • Schema validation: jsonschema validates 10,000 JSON documents against a schema in 2-10 seconds. pydantic validates the same volume in 0. 5-2 seconds.
  • Deep clone performance: copy. deepcopy() on a 1MB Python object takes 50-200ms. json. loads(json. dumps(obj)) takes 30-80ms but loses non-serializable types.
  • Sort performance: Python sorted() on 1M integers takes 200-400ms. umpy.sort() on the same array takes 50-100ms. JavaScript Array.sort() on 1M numbers takes 100-300ms (V8 Timsort)
  • Diff performance: difflib comparing two 10,000-line files takes 500ms-2s. deepdiff comparing two 1MB JSON objects takes 200ms-1s.
  • Regex performance: compiled regex in Python matches 1M strings in 50-200ms. Uncompiled regex takes 2-5x longer.

Testing Strategy

  • Test with edge-case data: empty objects, null values, nested arrays, Unicode strings, very large numbers (>2^53), and mixed-type arrays.
  • Test serialization round-trips: serialize an object, deserialize it, and compare. Round-trip testing catches data loss from type coercion (e. g.
  • Test schema validation failures: verify that invalid data is rejected with clear error messages.
  • Test with adversarial input: deeply nested JSON (10,000 levels), huge strings (1MB+), many keys (100,000+), and duplicate keys.
  • Test sort stability: verify that equal elements maintain their original order. Python’s sorted() is stable. JavaScript’s Array. sort() is stable in V8 since ES2019.
  • Test regex against malicious input: patterns like (a+)+b cause catastrophic backtracking on input like aaaaaaaaaaaaaaaaaaa!.

Cost Estimation

  • Validation overhead: schema validation adds 5-20% latency to request processing. For a service handling 10,000 req/s, this costs 1-2 extra CPU cores (-100/month).
  • Memory for large JSON: a 500MB JSON file uses 2-3GB in memory after parsing (Python dict overhead).
  • Caching infrastructure: Redis for caching validated data costs -200/month for a 10GB cache. Memcached is cheaper but lacks persistence.
  • Development cost: writing custom validators takes 4-16 hours per data type. Using pydantic or zod reduces this to 1-2 hours.
  • Serialization format tradeoffs: JSON is human-readable but 2-5x larger than binary formats.

Monitoring and Observability

  • Validation error rate: track the percentage of inputs that fail validation. Alert when error rate exceeds 5%.
  • Serialization duration: monitor time spent serializing/deserializing.
  • Cache hit rate: if caching validated data, monitor hit rate.
  • Memory usage of data structures: monitor peak memory after loading large JSON objects.
  • Regex execution time: log slow regex operations (>100ms). Slow regexes on user input are a DoS vector.

Deployment Checklist

  • Set maximum payload size: reject JSON payloads larger than 1MB (or appropriate limit) at the load balancer. Return HTTP 413 for oversized payloads
  • Configure schema versioning: include a schema version field in validated data. Reject data with unknown versions to prevent silent schema drift
  • Set recursion depth limits: for recursive validation or serialization, set a maximum depth (e.g., 100). Reject data that exceeds the limit to prevent stack overflow
  • Enable caching for validated data: cache validation results with a TTL. Use the raw input hash as the cache key. Invalidate on schema changes
  • Configure error responses: return structured validation errors with field paths and messages. Do not expose internal schema details in error responses
  • Set regex timeouts: use e.TIMEOUT (Python 3.11+) or run regex in a separate process with a timeout. Kill regex operations that exceed 1 second

Security Considerations

  • Prototype pollution via JSON merge: merging user-supplied JSON with proto or constructor keys can pollute JavaScript object prototypes.
  • Deserialization attacks: pickle. loads() in Python and unserialize() in PHP execute arbitrary code. Never deserialize untrusted data with these formats.
  • Regex DoS (ReDoS): patterns with nested quantifiers like (a+)+ cause exponential backtracking. An attacker can hang the server with a 30-character input.
  • JSON injection via key collision: duplicate keys in JSON ({“role”: “user”, “role”: “admin”) are handled differently by parsers. Python uses the last value, JavaScript uses the last value, but some parsers use the first.
  • Cache poisoning via validation bypass: if validation results are cached by input hash, an attacker who finds a hash collision can inject a cached “valid” result for invalid input.
  • Type confusion in dynamic languages: isinstance(x, int) returns True for True in Python (bool is a subclass of int).
  • Information leakage in error messages: validation errors that include schema details, internal field names, or stack traces help attackers understand the system.
  • Deep clone bypassing security checks: if a security-sensitive object is cloned and the clone skips validation, an attacker can modify the clone to bypass checks.
  • Sort comparator injection: if sort comparators come from user input, an attacker can provide a comparator that throws or hangs.
  • Diff leaking sensitive data: if diff output is logged or displayed, it may expose sensitive fields (passwords, tokens).
  • Cache key enumeration: if cache keys are sequential or predictable, an attacker can enumerate cached data.
  • Regex-based input validation bypass: ^pattern$ with e.DOTALL allows . to match newlines, potentially bypassing line-based validation. Use e.ASCII and explicit anchors for security-sensitive regexes

Variants and Alternatives

  • Schema-first vs code-first validation: JSON Schema, OpenAPI, and Protobuf define schemas in a language-agnostic format. Pydantic, zod, and joi define schemas in code.
  • Strict vs lenient validation: strict validation rejects unknown fields. Lenient validation ignores them. For APIs, strict validation prevents client errors from typos.
  • Deep copy vs shallow copy vs structural sharing: deep copy duplicates everything (expensive, safe). Shallow copy shares references (fast, unsafe for mutation). Structural sharing (used in immutable.
  • In-place sort vs copy sort: list. sort() sorts in-place (0 extra memory). sorted() returns a new list (O(n) memory). For large datasets, in-place sort is preferred.
  • Centralized vs distributed caching: Redis/Memcached are centralized caches shared across instances. In-process caches (LRU, functools. lru_cache) are faster but not shared.
  • Sync vs async validation: synchronous validation blocks the event loop. Async validation allows concurrent validation of multiple payloads.

Common Pitfalls in Production

  • Schema evolution breaks: adding a required field breaks existing clients. Removing a field breaks consumers that depend on it.
  • Validation order matters: validate format first (cheap), then type (medium), then business rules (expensive).
  • Silent type coercion: int(“3. 14”) raises ValueError but loat(“3”) succeeds. JSON parsers coerce strings to numbers in some languages.
  • Cache stampede: when a cache entry expires, all concurrent requests hit the backend simultaneously.
  • Deep copy performance traps: copy. deepcopy() on objects with circular references causes infinite recursion.
  • Sort instability with custom keys: Python’s sorted() is stable, but custom key functions that return equal values for different items can produce unexpected orderings.

Integration Patterns

  • API request validation pipeline: validate request body against schema (pydantic/zod) -> sanitize input (strip whitespace, normalize encoding) -> authorize (check permissions) -> process.
  • Event-driven data processing: when data changes, publish an event. Consumers validate and process the event independently.
  • CQRS with separate read/write models: write model validates and stores data. Read model projects data into optimized query structures. Validation happens only on the write side.
  • Data contract enforcement: define data contracts between services using JSON Schema or Protobuf. Validate at both producer and consumer sides.
  • Batch validation with reporting: validate 10,000+ records in batch.
  • Real-time validation with feedback: validate data as it arrives. Send immediate feedback to the data source (API response, UI error message).

Error Handling and Recovery

  • Validation error aggregation: collect all validation errors for a single input, not just the first one. Return all errors to the client so they can fix everything in one round-trip. Pydantic supports this with ValidationError.
  • Retry with backoff for transient failures: if validation fails due to a transient dependency (e. g. , reference data service is down), retry with exponential backoff.
  • Circuit breaker for validation dependencies: if a reference data service (needed for validation) is down, open a circuit breaker.
  • Compensating transactions for validation failures: if validation fails after partial processing (e. g.
  • Dead letter queue for invalid records: records that fail validation go to a dead letter queue for manual inspection.
  • Schema evolution with backward compatibility: when updating a schema, ensure backward compatibility. New required fields must have defaults. Removed fields should be optional for one release cycle before deletion.

Tooling and Ecosystem

  • Pydantic: Python data validation library. 30M+ downloads/month. Type-safe models with automatic validation. Used by FastAPI. v2 is 5-50x faster than v1 (Rust core).
  • zod: TypeScript-first schema validation. 20M+ downloads/month. Type inference from schemas. Composable with z. union, z. intersection.
  • JSON Schema: language-agnostic validation specification. Supported by 50+ libraries across languages. Draft 2020-12 is the latest.
  • msgpack: binary serialization format. 2-5x smaller and faster than JSON. Libraries for 50+ languages.
  • Immer: JavaScript immutable state library. Structural sharing with a mutable draft API. 10M+ downloads/month.
  • jsondiffpatch: JavaScript library for deep diffing and patching JSON objects. Supports arrays, nested objects, and reverse patches.

Best Practices Summary

  • Validate at system boundaries (API entry, file import, message consumption). Trust internal data
  • Use strict validation for user input, lenient validation for internal data pipelines
  • Prefer schema-first design (JSON Schema, Protobuf) for cross-service contracts
  • Cache validation results by input hash to avoid redundant processing
  • Use Decimal for money, int for counts, str for IDs. Never use loat for exact values
  • Log validation failures with field path, value, and expected type for debugging

Troubleshooting

  • Pipeline output does not match expectations: validate input schemas, intermediate states, and row counts at each step.
  • Data quality degrades over time: add data validation checks and anomaly detection. Define SLIs for freshness, completeness, and accuracy.
  • Job fails intermittently: look for race conditions, external dependencies, and resource contention. Retry with idempotency and bounded backoff.
  • Schema changes break consumers: use schema registries and backward-compatible evolution.
  • Storage costs grow unexpectedly: audit partition retention, compression, and duplicate copies. Archive cold data and set lifecycle policies.

Further Reading

  • Official documentation: check the current reference for the framework or tool used.
  • Related guides: explore the data and java guides for deeper coverage.
  • Complementary patterns: review design patterns applicable to your technology stack.
  • Public postmortems: study real incidents from teams that faced similar production issues.

Production Notes

  • Deploy gradually using canary or blue-green to catch regressions early.
  • Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
  • Document the rollback in the runbook; test the procedure in staging at least once per quarter.
  • Review structured logs with correlation IDs to trace requests end-to-end during incidents.

Key Takeaways

  • Apply sort an array when you need a practical solution for your use case.
  • Monitor performance after implementation; measure latency, errors, and resource usage before and after.
  • Check the Troubleshooting section for common failures; most have documented root causes with fixes.
  • Keep dependencies updated and run tests in CI to prevent production regressions.

Common Production Pitfalls

  • Copying the example without adapting it to real data volumes and failure modes.
  • Skipping load and error-injection tests before the first production deployment.
  • Hard-coding values that should be configurable per environment.
  • Forgetting to add logging and monitoring at each step.
  • Deploying without a rollback plan or a tested backup strategy.
  • Assuming the minimal example will scale without adding caching or batching.
  • Not documenting the version and configuration used in production.
  • Letting the recipe sit unchanged when dependencies or scale evolve.

Frequently Asked Questions

Why does [10, 2].sort() return [10, 2] in JavaScript?

The default sort() converts elements to strings and compares UTF-16 code units. "10" comes before "2" lexicographically. Always pass (a, b) => a - b for numeric sorts.

How do I sort by multiple fields?

In Python, return a tuple from the key function: sorted(users, key=lambda u: (u.country, u.age)). In JavaScript, chain comparisons: (a, b) => a.country.localeCompare(b.country) || a.age - b.age.

Is in-place sorting faster than creating a new sorted copy?

Slightly, because it avoids allocating a new array. However, for most applications the difference is negligible. Prefer immutability unless profiling shows a bottleneck.