Skip to content

PyDP silently truncates and converts np.float32 into int64, leading to sensitivity underestimation #499

Open

Description

@TedTed

In the code that registers the bindings with the C++ building blocks library, the int64 binding is defined before the double binding. This has a surprising consequence: pybind11, after doing a pass that sees if the input type matches the declared type, it then tries overloads in order with an implicit conversion path. The int caster apparently accepts a np.float32 and silently truncates its input, so values of this type automatically use the int64 binding.

Silent truncation predictably negative consequences on the sensitivity analysis: 0.95 becomes 0 and 1.05 becomes 1, so sensitivity increases unexpectedly. This is observable in PyDP:

import numpy as np
from pydp.algorithms.numerical_mechanisms import LaplaceMechanism

mechanism = LaplaceMechanism(epsilon=1.0, sensitivity=0.1)
print("np.float32, D :", [mechanism.add_noise(np.float32(0.95)) for _ in range(10)])
print("np.float32, D':", [mechanism.add_noise(np.float32(1.05)) for _ in range(10)])

and also affects PipelineDP via add_dp_noise, or VECTOR_SUM with float32 values. This happens even though the value is multiplied by 1.0, maybe to try and cast it to float? Sadly this doesn't work, since 1.0 * np.float32(1.7) still has type np.float32.

Here's a repro for add_dp_noise:

import numpy as np
import pipeline_dp

for name, value in (("D ", np.float32(0.95)), ("D'", np.float32(1.05))):
    releases = []
    for _ in range(10):
        accountant = pipeline_dp.NaiveBudgetAccountant(total_epsilon=1, total_delta=0)
        engine = pipeline_dp.DPEngine(accountant, pipeline_dp.LocalBackend())
        params = pipeline_dp.aggregate_params.AddDPNoiseParams(noise_kind=pipeline_dp.NoiseKind.LAPLACE, l0_sensitivity=1,
                                                               linf_sensitivity=0.1)
        result = engine.add_dp_noise([("partition", value)], params)
        accountant.compute_budgets()
        releases.append(list(result)[0][1])
    print(name, releases)

other PipelineDP aggregations (sum, mean, variance) convert to float64 and so aren't vulnerable to this.

This issue doesn't happen with np.float64, because np.float64 is a subclass of Python's native floats (while np.float32 is not). Therefore, the right binding is selected by pybind11 during the first pass.

Registering the double binding first would fix it.

Activity

  1. added
    Type: Bug 🐛Some functionality not working in the codebase as intended
    on Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type: Bug 🐛Some functionality not working in the codebase as intended

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions