{"next_page": 2, "previous_page": null, "max_next_page": 10, "posts": [{"oid": "useslowtruth", "title": "useSlowTruth - React hook to throttle booleans", "pub_date": "2026-08-10T15:09:04.899Z", "comments": 0, "categories": ["React"], "html": "<p>I use this React hook to reduce the display of spinner animation icons in cases where a little patience means we don't really need to bother indicating that something is loading.</p>\n<h3 id=\"the-problem\"><a class=\"toclink\" href=\"#the-problem\">The problem</a></h3>\n<p>For example, you might have a TanStack Query that makes XHR requests to the backend, and that backend is generally fast. Many times, it finishes in tens or hundreds of milliseconds. If you prescriptively always show the loading spinner icon, or whatever you might have, <strong>it's going to flicker and cause confusion</strong>.</p>\n<p>For example, your use of <code>useQuery</code> might look like this:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-keyword\">function</span> <span class=\"hljs-title function_\">MyComponent</span>(<span class=\"hljs-params\"></span>) {\n  <span class=\"hljs-keyword\">const</span> { data, isPending, error } = <span class=\"hljs-title function_\">useQuery</span>({\n    <span class=\"hljs-attr\">queryKey</span>: [<span class=\"hljs-string\">&quot;something&quot;</span>],\n    <span class=\"hljs-attr\">queryFn</span>: fetcher,\n  })\n\n  <span class=\"hljs-keyword\">return</span> <span class=\"language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div</span>&gt;</span>\n    {isPending &amp;&amp; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Spinner</span>/&gt;</span>}\n    {error &amp;&amp; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Alert</span> <span class=\"hljs-attr\">error</span>=<span class=\"hljs-string\">{error}/</span>&gt;</span>}\n    {data &amp;&amp; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Tabular</span> <span class=\"hljs-attr\">data</span>=<span class=\"hljs-string\">{data}</span> /&gt;</span>}\n  <span class=\"hljs-tag\">&lt;/<span class=\"hljs-name\">div</span>&gt;</span></span>\n}\n</code></pre>\n\n<p>The problem is that if <code>isPending</code> is only <code>true</code> for a very short time, you run the risk of displaying the <code>&lt;Spinner/&gt;</code> so briefly that it just becomes a flickering blur to the user.</p>\n<h3 id=\"the-solution\"><a class=\"toclink\" href=\"#the-solution\">The solution</a></h3>\n<p>The hook code looks like this:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-keyword\">import</span> { useEffect, useState } <span class=\"hljs-keyword\">from</span> <span class=\"hljs-string\">&quot;react&quot;</span>;\n\n<span class=\"hljs-keyword\">type</span> <span class=\"hljs-title class_\">Options</span> = {\n  <span class=\"hljs-attr\">delay</span>?: <span class=\"hljs-built_in\">number</span>;\n};\n\n<span class=\"hljs-comment\">/**\n * A hook that throttles the truth. Useful when you want something to be true\n * only if it&#x27;s been true for a certain delay in milliseconds. Example use:\n *\n *   const stillLoading = useSlowTruth(isLoading);\n *\n * If the value of `isLoading` quickly changes from false, to true, to false;\n * the value of `stillLoading` will remain false the whole time.\n *\n * <span class=\"hljs-doctag\">@param</span> initialState boolean\n * <span class=\"hljs-doctag\">@param</span> <span class=\"hljs-variable\">options</span>\n * <span class=\"hljs-doctag\">@returns</span> a single boolean that is a delayed mirror of the input, if it&#x27;s true\n */</span>\n<span class=\"hljs-keyword\">export</span> <span class=\"hljs-keyword\">function</span> <span class=\"hljs-title function_\">useSlowTruth</span>(<span class=\"hljs-params\"><span class=\"hljs-attr\">initialState</span>: <span class=\"hljs-built_in\">boolean</span>, { delay = <span class=\"hljs-number\">1000</span> }: <span class=\"hljs-title class_\">Options</span></span>) {\n  <span class=\"hljs-keyword\">const</span> [isTrue, setIsTrue] = <span class=\"hljs-title function_\">useState</span>(initialState);\n  <span class=\"hljs-title function_\">useEffect</span>(<span class=\"hljs-function\">() =&gt;</span> {\n    <span class=\"hljs-keyword\">let</span> mounted = <span class=\"hljs-literal\">true</span>;\n    <span class=\"hljs-keyword\">let</span> <span class=\"hljs-attr\">timer</span>: <span class=\"hljs-built_in\">number</span> | <span class=\"hljs-literal\">null</span> = <span class=\"hljs-literal\">null</span>;\n    <span class=\"hljs-keyword\">if</span> (initialState) {\n      timer = <span class=\"hljs-variable language_\">window</span>.<span class=\"hljs-built_in\">setTimeout</span>(<span class=\"hljs-function\">() =&gt;</span> {\n        <span class=\"hljs-keyword\">if</span> (mounted) {\n          <span class=\"hljs-title function_\">setIsTrue</span>(<span class=\"hljs-literal\">true</span>);\n        }\n      }, delay);\n    } <span class=\"hljs-keyword\">else</span> {\n      <span class=\"hljs-keyword\">if</span> (timer !== <span class=\"hljs-literal\">null</span>) {\n        <span class=\"hljs-variable language_\">window</span>.<span class=\"hljs-built_in\">clearTimeout</span>(timer);\n      }\n      <span class=\"hljs-title function_\">setIsTrue</span>(<span class=\"hljs-literal\">false</span>);\n    }\n    <span class=\"hljs-keyword\">return</span> <span class=\"hljs-function\">() =&gt;</span> {\n      <span class=\"hljs-keyword\">if</span> (timer !== <span class=\"hljs-literal\">null</span>) {\n        <span class=\"hljs-variable language_\">window</span>.<span class=\"hljs-built_in\">clearTimeout</span>(timer);\n      }\n      mounted = <span class=\"hljs-literal\">false</span>;\n    };\n  }, [initialState, delay]);\n  <span class=\"hljs-keyword\">return</span> isTrue;\n}\n</code></pre>\n\n<p>I put together a demo app here: <a href=\"https://github.com/peterbe/use-slow-truth-demo\">https://github.com/peterbe/use-slow-truth-demo</a></p>\n<h3 id=\"the-usage\"><a class=\"toclink\" href=\"#the-usage\">The usage</a></h3>\n<p>This change</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-addition\">+import { useSlowTruth } from &quot;./useSlowTruth&quot;</span>\n<span class=\"hljs-addition\">+</span>\nfunction MyComponent() {\n  const { data, isPending, error } = useQuery({\n    queryKey: [&quot;something&quot;],\n    queryFn: fetcher,\n  })\n\n<span class=\"hljs-addition\">+ const isStillPending = useSlowTruth(isPending, { delay: 300 })</span>\n\n  return &lt;div&gt;\n<span class=\"hljs-deletion\">-   {isPending &amp;&amp; &lt;Spinner/&gt;}</span>\n<span class=\"hljs-addition\">+   {isStillPending &amp;&amp; &lt;Spinner/&gt;}</span>\n    {error &amp;&amp; &lt;Alert error={error}/&gt;}\n    {data &amp;&amp; &lt;Tabular data={data} /&gt;}\n  &lt;/div&gt;\n}\n</code></pre>\n\n<p>Now, <em>only</em> if the XHR query takes <em>longer than 300ms</em> does it show the <code>&lt;Spinner/&gt;</code> component.</p>", "url": "https://github.com/peterbe/use-slow-truth-demo", "disallow_comments": false, "split": null}, {"oid": "cracked-my-callaway-driver", "title": "Cracked my Callaway driver", "pub_date": "2026-07-28T10:43:08.993Z", "comments": 0, "categories": [], "html": "<p>Heard something chip loose in it the other day. And now, on the range, it cracked open.</p>", "url": null, "disallow_comments": false, "split": null}, {"oid": "claude-opus-is-10x-faster-than-openai-gpt-5-at-non-streaming-completions", "title": "Claude Opus is 10x faster than OpenAI GPT 5 at non-streaming completions", "pub_date": "2026-07-24T15:48:18.297Z", "comments": 0, "categories": ["Python", "AI"], "html": "<p>This picture summarizes it well:</p>\n<p><a href=\"/cache/72/7a/727a021ed23a566f90dcb07968a07b4c.png\"><img src=\"/cache/72/7a/727a021ed23a566f90dcb07968a07b4c.png\" class=\"fullsize\"></a></p>\n<p>Here on my blog, for <a href=\"/plog/blogitem-040601-1\">this popular blog post</a> I get a lot of comments. 28k blog comments over the years. Some of them are terribly written and hard to understand, so I let AI suggest a rewrite. That code that sends the blog post comment to AI, I actually fire off three times: once with OpenAI <code>gpt-5</code>, once with OpenAI <code>gpt-5-mini</code>, and once with Claude <code>claude-opus-4.8</code>. I use my human eyes and judgement to evaluate the results, and I can tell you they do equally well. Only the slightest differences.</p>\n<p>The surprising thing is how amazingly slow OpenAI's <code>gpt-5</code> is! It's nearly 10x slower than <code>claude-opus-4.8</code>. What's up with that!?</p>\n<p>It's also clear that the latency difference between <code>gpt-5-mini</code> and <code>gpt-5</code> is significant. At the time of writing, the input token price difference between <code>gpt-5.4</code> and <code>gpt-5.4-mini</code> is $2.50 compared to $0.75! That's a 3x difference.</p>\n<h3 id=\"conclusion\"><a class=\"toclink\" href=\"#conclusion\">Conclusion</a></h3>\n<ul>\n<li>\n<p>If you're constructing a prompt the API, use Claude.</p>\n</li>\n<li>\n<p>If you have to use OpenAI, consider the <code>mini</code> model because it's <em>both</em> cheaper <em>and</em> faster.</p>\n</li>\n</ul>\n<h3 id=\"bonus\"><a class=\"toclink\" href=\"#bonus\">Bonus</a></h3>\n<p>Before I added Claude, I used to use <code>litellm</code> to wrap OpenAI's models. The code looks like this:</p>\n<pre><code class=\"hljs\">\nresponse = litellm.completion(\n    model=<span class=\"hljs-string\">&quot;openai-gpt-5&quot;</span>,\n    api_key=settings.OPENAI_API_KEY,\n    messages=my_prompt_messages,\n)\n</code></pre>\n\n<p>Unlike, if you use the native OpenAI Python SDK the invocation looks like this:</p>\n<pre><code class=\"hljs\">\nclient = openai.OpenAI(api_key=settings.OPENAI_API_KEY)\nresponse = client.responses.create(\n    model=<span class=\"hljs-string\">&quot;gpt-5,\n    input=my_prompt_messages,\n)\n</span></code></pre>\n\n<p>I measured the difference, in speed, where I compare using the OpenAI SDK versus the <code>litellm</code> wrapper and the difference looks like this:</p>\n<p><a href=\"/cache/7b/c0/7bc0bdb8d1a10f3edf6d35a75c998541.png\"><img src=\"/cache/7b/c0/7bc0bdb8d1a10f3edf6d35a75c998541.png\" class=\"fullsize\"></a></p>\n<p>Granted, in June I \"only\" did a bit over 30 of these calls, but strangely there's a difference!<br />\nI don't have the intricate knowledge to understand why the <code>litellm</code> makes the total time different from using the SDK. (not sure I care either!)</p>\n<p>Either way, I'm moving away from <code>litellm</code> and only use the SDKs provided by Claude and OpenAI. Feels safer given the CVEs we've seen this year on <code>litellm</code>.</p>", "url": null, "disallow_comments": false, "split": null}, {"oid": "best-django-redis-configuration", "title": "Best Django Redis configuration", "pub_date": "2026-07-20T19:11:05.415Z", "comments": 0, "categories": [], "html": "<p>Latest <a href=\"/plog/best-django-redis-configuration-for-speed-and-size\">blog post</a> about different compressors for Django Redis.</p>", "url": "https://www.peterbe.com/plog/best-django-redis-configuration-for-speed-and-size", "disallow_comments": false, "split": null}, {"oid": "best-django-redis-configuration-for-speed-and-size", "title": "Best Django Redis configuration for speed and size", "pub_date": "2026-07-19T20:01:27.016Z", "comments": 0, "categories": ["Django", "Python"], "html": "<p>In 2017 I wrote <a href=\"/plog/fastest-redis-optimization-for-django\">\"Fastest Redis configuration for Django\"</a>. Now, I've made an update for 2026 that focuses on comparing default Redis with three compressors:</p>\n<ul>\n<li><code>zlib</code></li>\n<li><code>lzma</code></li>\n<li><code>zstd</code></li>\n</ul>\n<p>The results on macOS are:</p>\n<pre><code>                        TIMES        AVERAGE   MEDIAN (P50)   MEDIAN (P90)         STDDEV\ndefault                    198        0.181ms        0.176ms        0.216ms        0.038ms\nzlib                       213        0.190ms        0.182ms        0.237ms        0.039ms\nlzma                       134        0.273ms        0.272ms        0.332ms        0.042ms\nzstd                       202        0.192ms        0.180ms        0.243ms        0.046ms\n\nBest Means (shorter better)\n###############################################################################\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588                        0.181  default\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588                      0.190  zlib\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588  0.273  lzma\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588                     0.192  zstd\n\nBest Medians (shorter better)\n###############################################################################\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588                         0.176  default\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588                       0.182  zlib\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588  0.272  lzma\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588                        0.180  zstd\n\nSize of Data Saved (shorter better)\n###############################################################################\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588  5151  default\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588                                        2151  zlib\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588                                                 1420  lzma\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588                                          2010  zstd\n\nSize of Data without Default (shorter better)\n###############################################################################\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588  2151  zlib\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588                         1420  lzma\n\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588       2010  zstd\n</code></pre>\n<p>The \"conclusion\", on macOS, is that <code>lzma</code> is slowest but compresses the best. On Ubuntu (in GitHub Actions), the fastest of the compressors is <code>zstd</code> and the best compression is <code>lzma</code>.</p>\n<p>But it's a very small difference.</p>\n<p>The type of data you store in Redis will matter for <em>your</em> use case. In this experiment, it's storing strings of numbers. Each number is about 10 characters long.</p>", "url": null, "disallow_comments": false, "split": null}, {"oid": "how-to-use-a-listtuplearray-in-django-with-a-raw-sql-cursor", "title": "How to use a list/tuple/array in Django with a raw SQL cursor", "pub_date": "2026-07-14T13:14:40.383Z", "comments": 0, "categories": ["Django", "Python"], "html": "<p>This does <strong><em>not</em></strong> work:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-keyword\">from</span> django.db <span class=\"hljs-keyword\">import</span> connection\n\nlist_of_values = [<span class=\"hljs-number\">1</span>, <span class=\"hljs-number\">2</span>, <span class=\"hljs-number\">3</span>]\n<span class=\"hljs-keyword\">with</span> connection.cursor() <span class=\"hljs-keyword\">as</span> cursor:\n    cursor.execute(<span class=\"hljs-string\">&quot;&quot;&quot;\n        SELECT *\n        FROM my_model_table\n        WHERE some_value IN %s\n    &quot;&quot;&quot;</span>, [\n        <span class=\"hljs-built_in\">tuple</span>(list_of_values),\n    ])\n    results = cursor.fetchall()\n</code></pre>\n\n<p>It will give you:</p>\n<pre>django.db.utils.ProgrammingError: syntax error at or near &quot;&#x27;(1,2,3)&#x27;&quot;\nLINE 4:         WHERE id IN &#x27;(1,2,3)&#x27;</pre>\n\n<p>It <em>used</em> to work with <code>psycopg</code> v2. Now, in psycopg v3, you have to use the <code>ANY</code> operator. See <a href=\"https://www.psycopg.org/psycopg3/docs/basic/from_pg2.html#you-cannot-use-in-s-with-a-tuple\">\"You cannot use IN %s with a tuple\"</a></p>\n<p>This <em>will</em> work:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-keyword\">from</span> django.db <span class=\"hljs-keyword\">import</span> connection\n\nlist_of_values = [<span class=\"hljs-number\">1</span>, <span class=\"hljs-number\">2</span>, <span class=\"hljs-number\">3</span>]\n<span class=\"hljs-keyword\">with</span> connection.cursor() <span class=\"hljs-keyword\">as</span> cursor:\n    cursor.execute(\n        <span class=\"hljs-string\">&quot;&quot;&quot;\n        SELECT *\n        FROM my_model_table\n        WHERE some_value = ANY(%s)\n    &quot;&quot;&quot;</span>,\n        [\n            list_of_values,\n        ],\n    )\n    results = cursor.fetchall()\n</code></pre>\n\n<p>Note the <code>ANY(%s)</code>, and instead of a list that has a tuple, it's a list that has a list.</p>\n<h4 id=\"what-about-a-list-of-strings\"><a class=\"toclink\" href=\"#what-about-a-list-of-strings\">What About a List of Strings</a></h4>\n<p>Consider...</p>\n<pre><code class=\"hljs\">\nfrom django.db import connection\n\n<span class=\"hljs-deletion\">-list_of_values = [1, 2, 3]</span>\n<span class=\"hljs-addition\">+list_of_values = [&#x27;foo&#x27;, &#x27;bar&#x27;, &#x27;fiz&#x27;]</span>\nwith connection.cursor() as cursor:\n    cursor.execute(\n        &quot;&quot;&quot;\n        SELECT *\n        FROM my_model_table\n        WHERE some_value = ANY(%s)\n    &quot;&quot;&quot;,\n        [\n            list_of_values,\n        ],\n    )\n    results = cursor.fetchall()\n</code></pre>\n\n<p>That will result in:</p>\n<pre>django.db.utils.DataError: invalid input syntax for type integer: &quot;foo&quot;\nLINE 4:         WHERE some_value = ANY(&#x27;{foo,bar,fiz}&#x27;)</pre>\n\n<p>My solution was to rewrite the SQL string itself and treat each value as a parameter each. In other words, the SQL string, before being sent to <code>cursor.execute(...)</code> will contain something like this:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-keyword\">AND</span> (\n  some_value <span class=\"hljs-operator\">=</span> <span class=\"hljs-operator\">%</span> <span class=\"hljs-keyword\">OR</span>\n  some_value <span class=\"hljs-operator\">=</span> <span class=\"hljs-operator\">%</span> <span class=\"hljs-keyword\">OR</span>\n  some_value <span class=\"hljs-operator\">=</span> <span class=\"hljs-operator\">%</span> <span class=\"hljs-keyword\">OR</span>\n  some_value <span class=\"hljs-operator\">=</span> <span class=\"hljs-operator\">%</span> <span class=\"hljs-keyword\">OR</span>\n  <span class=\"hljs-comment\">-- ...etc...</span>\n  some_value <span class=\"hljs-operator\">=</span> <span class=\"hljs-operator\">%</span>\n)\n</code></pre>\n\n<p>This will work and is safe:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-keyword\">from</span> django.db <span class=\"hljs-keyword\">import</span> connection\n\nlist_of_values = [<span class=\"hljs-string\">&quot;foo&quot;</span>, <span class=\"hljs-string\">&quot;bar&quot;</span>, <span class=\"hljs-string\">&quot;fiz&quot;</span>]\n<span class=\"hljs-keyword\">with</span> connection.cursor() <span class=\"hljs-keyword\">as</span> cursor:\n    cursor.execute(\n        <span class=\"hljs-string\">f&quot;&quot;&quot;\n        SELECT *\n        FROM my_model_table\n        WHERE (<span class=\"hljs-subst\">{<span class=\"hljs-string\">&quot; OR &quot;</span>.join([<span class=\"hljs-string\">&quot;some_value = %s&quot;</span> <span class=\"hljs-keyword\">for</span> _ <span class=\"hljs-keyword\">in</span> list_of_values])}</span>)\n    &quot;&quot;&quot;</span>,\n        list_of_values,\n    )\n    results = cursor.fetchall()\n</code></pre>", "url": null, "disallow_comments": false, "split": null}, {"oid": "driving-range-and-thunder-clouds", "title": "Driving range and thunder clouds", "pub_date": "2026-07-12T10:26:11.412Z", "comments": 0, "categories": [], "html": "", "url": null, "disallow_comments": false, "split": null}, {"oid": "4th-of-july-tablecloth-that-we-all-contributed-to", "title": "4th of July table cloth that we all contributed to", "pub_date": "2026-07-04T22:19:09.620Z", "comments": 0, "categories": [], "html": "", "url": null, "disallow_comments": false, "split": null}, {"oid": "potpourri-of-funly-cut-hot-dogs", "title": "Potpourri of fun\u2019ly cut hot dogs", "pub_date": "2026-07-04T20:28:04.320Z", "comments": 0, "categories": [], "html": "", "url": null, "disallow_comments": false, "split": null}, {"oid": "putted-in-my-first-ever-eagle", "title": "Putted in my first ever Eagle!", "pub_date": "2026-06-28T17:18:19.306Z", "comments": 0, "categories": [], "html": "", "url": null, "disallow_comments": false, "split": null}]}