laspy vs PDAL: When to Use Which

TL;DR: Use laspy when the job is reading, editing or writing LAS/LAZ fields in NumPy — headers, attributes, arrays from other tools — and you want a light pip install. Use PDAL when the job involves spatial algorithms (ground filtering, outliers, height above ground, rasterization), reprojection, many formats, streaming through multi-stage pipelines, or reproducible JSON pipelines in batch. For most production work, use both: PDAL for the heavy stages, laspy or NumPy for the custom steps in between.

# Context and Motivation

This guide is part of laspy and NumPy Workflows for LAS Data. Teams starting with Python LiDAR processing often ask which library to standardize on, and the question is framed as a choice when it is really a division of labour. The two tools have different centres of gravity. laspy is a file-format library: it knows everything about LAS and LAZ and nothing about algorithms. PDAL is a processing framework: it knows dozens of formats and a large catalogue of filters, and exposes them through pipelines. Picking the right one per task — and knowing how to pass data between them — saves both effort and memory.

File format library versus processing framework A two-axis chart. The horizontal axis runs from file-level operations to spatial algorithms; the vertical axis from lightweight install to full geospatial stack. laspy sits at file-level operations with a lightweight install. PDAL sits at spatial algorithms with a full geospatial stack including GDAL and PROJ. NumPy sits between them as the common data representation. file-level operations → spatial algorithms install footprint laspy pip, pure Python + lazrs PDAL C++, GDAL, PROJ, conda NumPy arrays

# Prerequisites and Assumptions

  • A task defined well enough to know whether it needs spatial neighbourhoods, reprojection or rasters.
  • An environment policy: pip-only environments favour laspy; conda or containers make PDAL easy.
  • Awareness that both read LAZ; PDAL additionally reads COPC, EPT, E57, PLY, text and many more formats.

# Step-by-Step Implementation

# Step 1 — Classify the task

Is it file-level (headers, fields, format conversion), point-wise (arithmetic on attributes), or spatial (neighbours, surfaces, rasters)?

# Step 2 — Check the environment constraints

Can you install PDAL with its GDAL and PROJ dependencies here? In a Lambda function or a minimal container, laspy may be the only realistic option.

# Step 3 — Consider data size and memory

laspy reads whole files or chunks you manage yourself; PDAL can stream through a multi-stage pipeline with bounded memory.

# Step 4 — Consider reproducibility and review

PDAL pipelines are JSON documents a reviewer can read and CI can validate; laspy logic lives in Python code.

# Step 5 — Combine at the array boundary

Where a task needs both, pass NumPy arrays between them rather than writing intermediate files.

# Complete Working Example

A workflow that uses each tool for what it does best: PDAL computes ground and height above ground, NumPy applies a custom per-point rule, and laspy writes the result with a new extra dimension.

python
"""PDAL for spatial stages, NumPy for a custom rule, laspy for the final file."""
from __future__ import annotations

import json

import laspy
import numpy as np
import pdal
from pyproj import CRS

SRC = "tiles/t_0431.laz"

# 1. PDAL: noise removal, ground classification, height above ground (spatial work).
p = pdal.Pipeline(json.dumps({"pipeline": [
    SRC,
    {"type": "filters.range", "limits": "Classification![7:7],Classification![18:18]"},
    {"type": "filters.smrf", "slope": 0.15, "window": 18, "threshold": 0.5},
    {"type": "filters.hag_nn", "count": 2},
]}))
p.execute()
a = p.arrays[0]
crs_wkt = p.metadata["metadata"]["readers.las"]["srs"]["compoundwkt"]

# 2. NumPy: a project-specific rule PDAL has no stage for.
risk = np.clip((a["HeightAboveGround"] - 10.0) / 20.0, 0.0, 1.0).astype(np.float32)
risk[a["Classification"] == 6] = 0.0            # buildings are not fall-risk trees

# 3. laspy: write a LAS 1.4 file with the new dimension.
header = laspy.LasHeader(point_format=6, version="1.4")
header.scales = np.array([0.01, 0.01, 0.01])
header.offsets = np.floor([a["X"].min(), a["Y"].min(), a["Z"].min()])
header.add_extra_dim(laspy.ExtraBytesParams(name="fall_risk", type=np.float32))
header.add_crs(CRS.from_wkt(crs_wkt))
las = laspy.LasData(header)
las.x, las.y, las.z = a["X"], a["Y"], a["Z"]
las.intensity = a["Intensity"]
las.classification = a["Classification"]
las.return_number, las.number_of_returns = a["ReturnNumber"], a["NumberOfReturns"]
las.gps_time = a["GpsTime"]
las.fall_risk = risk
las.write("out/t_0431_fall_risk.laz")
print(f"{len(las.points):,} points written; {np.mean(risk > 0.5):.1%} high risk")

The last step could equally be a PDAL writers.las fed with the modified array; laspy is used here to show the hand-off in both directions. The metadata key path for the CRS can differ slightly between PDAL versions, so read it defensively in production code.

Each tool where it is strongest A three-stage flow. PDAL reads the tile and runs noise removal, SMRF and HAG. The resulting array passes to NumPy, where a custom fall-risk rule is computed. The array with the new field passes to laspy, which writes a LAS 1.4 file with an extra dimension and the CRS. PDAL read, noise, SMRF height above ground NumPy custom rule fall_risk array laspy LAS 1.4, extra dim CRS from PDAL metadata no intermediate files; arrays cross the boundaries

# Key Parameter Table

Task laspy PDAL Prefer
Read header, VLRs direct pdal info --metadata laspy
Change a field for all points NumPy assignment filters.assign either
Ground classification filters.smrf, filters.pmf, filters.csf PDAL
Height above ground filters.hag_nn PDAL
Reprojection via pyproj on arrays filters.reprojection PDAL
Rasterize to GeoTIFF writers.gdal PDAL
COPC spatial query CopcReader.query readers.copc either
Minimal serverless install pip only heavy laspy
Reviewable, versioned pipelines code JSON PDAL
Large files, many stages manual chunking streaming engine PDAL

# Verification

When a workflow switches tools mid-stream, verify at each boundary:

  • Counts. The array length leaving PDAL equals the number of points laspy writes.
  • Coordinates. Round-trip error at most half the output scale.
  • CRS. The output file’s CRS matches the input’s; laspy does not carry it automatically from PDAL arrays.

# Gotchas and Edge Cases

Field naming differs. PDAL uses Classification, ReturnNumber, GpsTime; laspy uses classification, return_number, gps_time. Map names explicitly at the boundary.

CRS does not travel with arrays. A NumPy array has no spatial reference. Carry it separately, as the example does from PDAL’s metadata, and set it on the laspy header or PDAL writer.

Dependency conflicts. A pip-installed pyproj next to a conda-installed PDAL can load two different PROJ libraries. Install both from one channel in a single environment.

Same fields, two naming schemes A two-column mapping. PDAL names on the left in CamelCase — Classification, ReturnNumber, NumberOfReturns, GpsTime, PointSourceId — map to laspy names on the right in snake_case — classification, return_number, number_of_returns, gps_time, point_source_id. PDALlaspy Classificationclassification ReturnNumberreturn_number NumberOfReturnsnumber_of_returns GpsTimegps_time PointSourceIdpoint_source_id

Reimplementing PDAL in NumPy. Writing your own ground filter or rasterizer in NumPy is tempting and nearly always slower and less robust than the equivalent PDAL stage. Reserve custom code for rules PDAL does not have.

# Frequently Asked Questions

Is laspy or PDAL better for Python LiDAR processing?

Neither is better in general. laspy excels at reading, editing and writing LAS and LAZ data as NumPy arrays with a light install. PDAL excels at spatial processing, reprojection, rasterization and multi-format, multi-stage pipelines. Most production workflows use both.

Can PDAL and laspy read the same files?

Yes. Both read LAS and LAZ, and both can query COPC. PDAL also reads many other point cloud formats.

Which is faster?

For reading and writing LAS or LAZ, they are broadly comparable, with laspy’s parallel LAZ backend competitive. For spatial algorithms, PDAL is far faster because laspy has none and NumPy reimplementations are usually slower.

How do I pass data from PDAL to laspy?

Execute the PDAL pipeline and take pipeline.arrays, then assign the fields to a laspy LasData object, mapping PDAL’s CamelCase names to laspy’s snake_case names and setting the CRS explicitly.