Skip to content

MAINT: numpy.i: Replace deprecated sprintf with snprintf#31056

Merged
charris merged 4 commits into
numpy:mainfrom
denproc:fix_sprintf_deprecation
Mar 28, 2026
Merged

MAINT: numpy.i: Replace deprecated sprintf with snprintf#31056
charris merged 4 commits into
numpy:mainfrom
denproc:fix_sprintf_deprecation

Conversation

@denproc

@denproc denproc commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

PR summary

This PR is for #31055.
The suggested changes in tools/swig/numpy.i replace deprecated sprintf with snprintf.

Similar issue was raised in #30997 and resolved in #30998.

First time committer introduction

Hi, I'm Denis Prokopenko, Researcher at King's College London. My main use of numpy is development of medical imaging methods. This particular PR was inspired by similar changes introduced in STIR UCL/STIR#1699.

AI Disclosure

No AI tools used

@denproc denproc changed the title MAINT: numpy.i: Replace deprecated 'sprintf' with snprintf MAINT: numpy.i: Replace deprecated sprintf with snprintf Mar 23, 2026
@jorenham jorenham linked an issue Mar 23, 2026 that may be closed by this pull request
@charris charris added the 09 - Backport-Candidate PRs tagged should be backported label Mar 24, 2026
Comment thread tools/swig/numpy.i Outdated
@denproc denproc mentioned this pull request Mar 24, 2026
6 tasks
@charris

charris commented Mar 27, 2026

Copy link
Copy Markdown
Member

@denproc AI does a nice job here. If you want to practice, that would be fine.

@denproc

denproc commented Mar 27, 2026

Copy link
Copy Markdown
Contributor Author

Hi @charris, I have addressed both snpintf, mentions in the original issue, and strcat, raised by @seberg, in the previsous commits.
I have also added additional validation of snprintf output to be extra safe as it could be negative or larger that buffer size.

Could you please let me know if there are any other outstanding issues and if I should also suggest megring it to maintenance branches for backport?

@denproc denproc requested a review from seberg March 27, 2026 09:15
@charris

charris commented Mar 27, 2026

Copy link
Copy Markdown
Member

@denproc I suggested AI because it produced cleaner, more readable code.

@denproc

denproc commented Mar 27, 2026

Copy link
Copy Markdown
Contributor Author

@charris, could you clarify what specifically could be improved for readability?
Is the concern mainly with the append logic, or would you prefer moving it into a helper function?

I initially tried to keep the changes minimal and local, but I’m happy to restructure it to reduce duplications if that’s preferred.

@charris

charris commented Mar 27, 2026

Copy link
Copy Markdown
Member

I find the code here messy and overly verbose. Why not try AI yourself for suggestions?

@denproc

denproc commented Mar 27, 2026

Copy link
Copy Markdown
Contributor Author

@charris, I did try an AI-generated rewrite from scratch, but it seemed to keep the same inline snprintf/buffer bookkeeping structure, so I'm not sure it's actually cleaner than the current patch.
That’s why I asked for clarification, I assumed you had a more specific direction in mind for simplifying the current changes.

Codex suggestions:

int require_dimensions_n(PyArrayObject* ary,
                           int*           exact_dimensions,
                           int            n)
  {
    int success = 0;
    int i;
    int written;
    char dims_str[255] = "";
    size_t dims_len = 0;
    for (i = 0; i < n && !success; i++)
    {
      if (array_numdims(ary) == exact_dimensions[i])
      {
        success = 1;
      }
    }
    if (!success)
    {
      for (i = 0; i < n-1; i++)
      {
        size_t remaining = sizeof(dims_str) - dims_len;
        written = snprintf(dims_str + dims_len,
                           remaining,
                           "%d, ",
                           exact_dimensions[i]);
        if (written < 0)
        {
          break;
        }
        if ((size_t)written >= remaining)
        {
          dims_len = sizeof(dims_str) - 1;
          break;
        }
        dims_len += (size_t)written;
      }
      if (n > 0)
      {
        size_t remaining = sizeof(dims_str) - dims_len;
        written = snprintf(dims_str + dims_len,
                           remaining,
                           " or %d",
                           exact_dimensions[n-1]);
        if (written >= 0 && (size_t)written < remaining)
        {
          dims_len += (size_t)written;
        }
        else if (written >= 0)
        {
          dims_len = sizeof(dims_str) - 1;
        }
      }
      PyErr_Format(PyExc_TypeError,
                   "Array must have %s dimensions.  Given array has %d dimensions",
                   dims_str,
                   array_numdims(ary));
    }
    return success;
  }

  /* Require the given PyArrayObject to have a specified shape.  If the
   * array has the specified shape, return 1.  Otherwise, set the python
   * error string and return 0.
   */
  int require_size(PyArrayObject* ary,
                   npy_intp*      size,
                   int            n)
  {
    int i;
    int written;
    int success = 1;
    char desired_dims[255] = "[";
    size_t desired_len = 1;
    char actual_dims[255] = "[";
    size_t actual_len = 1;
    for(i=0; i < n;i++)
    {
      if (size[i] != -1 &&  size[i] != array_size(ary,i))
      {
        success = 0;
      }
    }
    if (!success)
    {
      for (i = 0; i < n; i++)
      {
        size_t remaining = sizeof(desired_dims) - desired_len;
        if (size[i] == -1)
        {
          written = snprintf(desired_dims + desired_len,
                             remaining,
                             "*,");
        }
        else
        {
          written = snprintf(desired_dims + desired_len,
                             remaining,
                             "%ld,",
                             (long int)size[i]);
        }
        if (written < 0)
        {
          break;
        }
        if ((size_t)written >= remaining)
        {
          desired_len = sizeof(desired_dims) - 1;
          break;
        }
        desired_len += (size_t)written;
      }
      if (desired_len > 1)
      {
        desired_dims[desired_len - 1] = ']';
      }
      else if (desired_len < sizeof(desired_dims) - 1)
      {
        desired_dims[desired_len++] = ']';
        desired_dims[desired_len] = '\0';
      }
      for (i = 0; i < n; i++)
      {
        size_t remaining = sizeof(actual_dims) - actual_len;
        written = snprintf(actual_dims + actual_len,
                           remaining,
                           "%ld,",
                           (long int)array_size(ary,i));
        if (written < 0)
        {
          break;
        }
        if ((size_t)written >= remaining)
        {
          actual_len = sizeof(actual_dims) - 1;
          break;
        }
        actual_len += (size_t)written;
      }
      if (actual_len > 1)
      {
        actual_dims[actual_len - 1] = ']';
      }
      else if (actual_len < sizeof(actual_dims) - 1)
      {
        actual_dims[actual_len++] = ']';
        actual_dims[actual_len] = '\0';
      }
      PyErr_Format(PyExc_TypeError,
                   "Array must have shape of %s.  Given array has shape of %s",
                   desired_dims,
                   actual_dims);
    }
    return success;
  }

@charris

charris commented Mar 27, 2026

Copy link
Copy Markdown
Member

Note that the variable names don't clutter the code, keeps things on single lines, which makes it much easier to read. The difference I got using AI was similar:

diff --git a/tools/swig/numpy.i b/tools/swig/numpy.i
index abc1234..def5678 100644
--- a/tools/swig/numpy.i
+++ b/tools/swig/numpy.i
@@ -290,28 +290,40 @@ %fragment("NumPy_Array_Requirements",
    * number of dimensions. If the array has one of the specified number
    * of dimensions, return 1. Otherwise, set the python error string
    * and return 0.
    */
   int require_dimensions_n(PyArrayObject* ary,
                            int* exact_dimensions,
                            int n)
   {
     int success = 0;
     int i;
-    char dims_str[255] = "";
-    char s[255];
-    for (i = 0; i < n && !success; i++)
-    {
-      if (array_numdims(ary) == exact_dimensions[i])
-      {
-        success = 1;
-      }
-    }
-    if (!success)
-    {
-      for (i = 0; i < n-1; i++)
-      {
-        sprintf(s, "%d, ", exact_dimensions[i]);
-        strcat(dims_str,s);
-      }
-      sprintf(s, " or %d", exact_dimensions[n-1]);
-      strcat(dims_str,s);
+
+    /* Quick check */
+    for (i = 0; i < n; i++) {
+      if (array_numdims(ary) == exact_dimensions[i]) {
+        success = 1;
+        break;
+      }
+    }
+    if (success) return 1;
+
+    /* Build comma-separated list of dimensions safely with snprintf */
+    char dims_str[256] = "";
+    char *p = dims_str;
+    size_t remaining = sizeof(dims_str);
+
+    for (i = 0; i < n-1; i++) {
+      int written = snprintf(p, remaining, "%d, ", exact_dimensions[i]);
+      if (written < 0 || (size_t)written >= remaining) break;
+      p += written;
+      remaining -= (size_t)written;
+    }
+
+    /* Last dimension with "or " */
+    if (n > 0) {
+      snprintf(p, remaining, "or %d", exact_dimensions[n-1]);
+    }
+
       PyErr_Format(PyExc_TypeError,
                    "Array must have %s dimensions. Given array has %d dimensions",
                    dims_str,
                    array_numdims(ary));
     }
     return success;
   }
@@ -320,32 +332,38 @@ %fragment("NumPy_Array_Requirements",
    * array has the specified shape, return 1. Otherwise, set the python
    * error string and return 0.
    */
   int require_size(PyArrayObject* ary,
                    npy_intp* size,
                    int n)
   {
     int i;
     int success = 1;
-    size_t len;
-    char desired_dims[255] = "[";
-    char s[255];
-    char actual_dims[255] = "[";
+
     for(i=0; i < n; i++)
     {
       if (size[i] != -1 && size[i] != array_size(ary,i))
       {
         success = 0;
+        break;
       }
     }
-    if (!success)
-    {
-      for (i = 0; i < n; i++)
-      {
-        if (size[i] == -1)
-        {
-          sprintf(s, "*,");
-        }
-        else
-        {
-          sprintf(s, "%ld,", (long int)size[i]);
-        }
-        strcat(desired_dims,s);
-      }
-      len = strlen(desired_dims);
-      desired_dims[len-1] = ']';
-      for (i = 0; i < n; i++)
-      {
-        sprintf(s, "%ld,", (long int)array_size(ary,i));
-        strcat(actual_dims,s);
-      }
-      len = strlen(actual_dims);
-      actual_dims[len-1] = ']';
+
+    if (success) return 1;
+
+    /* Build desired and actual shape strings safely */
+    char desired_dims[256] = "[";
+    char actual_dims[256] = "[";
+    char s[64];
+
+    for (i = 0; i < n; i++)
+    {
+      if (size[i] == -1)
+      {
+        strncat(desired_dims, "*,", sizeof(desired_dims) - strlen(desired_dims) - 1);
+      }
+      else
+      {
+        snprintf(s, sizeof(s), "%ld,", (long int)size[i]);
+        strncat(desired_dims, s, sizeof(desired_dims) - strlen(desired_dims) - 1);
+      }
+
+      snprintf(s, sizeof(s), "%ld,", (long int)array_size(ary, i));
+      strncat(actual_dims, s, sizeof(actual_dims) - strlen(actual_dims) - 1);
+    }
+
+    /* Replace trailing comma with ']' */
+    size_t len = strlen(desired_dims);
+    if (len > 0) desired_dims[len-1] = ']';
+
+    len = strlen(actual_dims);
+    if (len > 0) actual_dims[len-1] = ']';
+
       PyErr_Format(PyExc_TypeError,
                    "Array must have shape of %s. Given array has shape of %s",
                    desired_dims,
                    actual_dims);
     }
     return success;
   }

I haven't checked it for correctness, I just wanted to see what it looked like. We should probably have #include <strings.h> for strncat if we keep it -- I note that you don't. It currently comes in indirectly in Python.h, but direct inclusion is recommended.

@denproc

denproc commented Mar 27, 2026

Copy link
Copy Markdown
Contributor Author

@charris thanks! To address the deprecation issue, I updated the code and replaced sprintf/strcat with snprintf/strncat, while keeping the changes minimal and clean, instead of replacing everything with just snprintf.

There are still potential improvements (avoiding repeated strlen calls and making truncation handling more explicit, which may add some verbosity), but I'm not sure if it is preferable to handle those in a separate PR. What do you think?

@charris charris merged commit e13e931 into numpy:main Mar 28, 2026
78 checks passed
@charris

charris commented Mar 28, 2026

Copy link
Copy Markdown
Member

Thanks @denproc . That was much easier to review for correctness with the minimal changes. The AI was doing some extra optimizations, but I doubt is matters here.

@charris

charris commented Mar 28, 2026

Copy link
Copy Markdown
Member

Note that the hot paths in both functions involved are already taken care of, speed in the error paths doesn't matter much.

@charris

charris commented Mar 28, 2026

Copy link
Copy Markdown
Member

The only remaining useful improvements I see in the AI diff are the comments, and the break in the hot loop. But not really worth addressing unless someone complains :)

charris pushed a commit to charris/numpy that referenced this pull request Mar 28, 2026
charris added a commit that referenced this pull request Mar 28, 2026
MAINT: numpy.i: Replace deprecated `sprintf` with `snprintf` (#31056)
@denproc denproc deleted the fix_sprintf_deprecation branch March 28, 2026 23:34
@charris charris removed the 09 - Backport-Candidate PRs tagged should be backported label Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MAINT: numpy.i: 'sprintf' is deprecated

4 participants