<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet type="text/xsl" href="/rss.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Python Developer Tooling Handbook – Security</title>
    <link>https://pydevtools.com/handbook/topics/security/</link>
    <description>Supply chain protection, attestations, and vulnerability scans for Python.</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <lastBuildDate>Wed, 08 Apr 2026 13:59:07 -0400</lastBuildDate>
    
	  <atom:link href="https://pydevtools.com/handbook/topics/security/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>How to Enable Ruff Security Rules</title>
      <link>https://pydevtools.com/handbook/how-to/how-to-enable-ruff-security-rules/</link>
      <pubDate>Wed, 13 May 2026 06:43:22 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/how-to/how-to-enable-ruff-security-rules/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://pydevtools.com/handbook/reference/ruff/&#34;&gt;Ruff&lt;/a&gt;&amp;rsquo;s &lt;code&gt;S&lt;/code&gt; rule group implements &lt;a href=&#34;https://docs.astral.sh/ruff/rules/#flake8-bandit-s&#34;target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;flake8-bandit&lt;/a&gt; security checks, catching hardcoded passwords, injection vulnerabilities, insecure hashing, and unsafe deserialization before they reach production.&lt;/p&gt;
&lt;h2&gt;Enable the Full Set&lt;span class=&#34;hx:absolute hx:-mt-20&#34; id=&#34;enable-the-full-set&#34;&gt;&lt;/span&gt;
    &lt;a href=&#34;#enable-the-full-set&#34; class=&#34;subheading-anchor&#34; aria-label=&#34;Permalink for this section&#34;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Add the &lt;code&gt;S&lt;/code&gt; group to the project&amp;rsquo;s &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code&#34;&gt;

&lt;div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ruff&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;lint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;extend-select&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;S&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;    &lt;span class=&#34;c&#34;&gt;# flake8-bandit security rules&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&#34;hextra-code-copy-btn-container  hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0&#34;&gt;
  &lt;button
    class=&#34;hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50&#34;
    title=&#34;Copy code&#34;
    aria-label=&#34;Copy code&#34;
    data-copied-label=&#34;Copied!&#34;
  &gt;
    &lt;div class=&#34;hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4&#34;&gt;&lt;/div&gt;
&lt;div class=&#34;hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4&#34;&gt;&lt;/div&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;This enables all flake8-bandit rules at once. Run the linter to see what it finds:&lt;/p&gt;</description>
    </item>
    <item>
      <title>How to Host Your Own Python Package Index</title>
      <link>https://pydevtools.com/handbook/how-to/how-to-host-your-own-python-package-index/</link>
      <pubDate>Wed, 13 May 2026 06:43:22 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/how-to/how-to-host-your-own-python-package-index/</guid>
      <description>&lt;p&gt;A self-hosted Python package index can save a team from PyPI outages, vendor lock-in, or restricted networks, but it also gives that team another service to run. Three tools cover almost every use case: &lt;a href=&#34;https://github.com/devpi/devpi&#34;target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;devpi&lt;/a&gt; for production multi-team setups, &lt;a href=&#34;https://github.com/pypiserver/pypiserver&#34;target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;pypiserver&lt;/a&gt; for a small team that needs somewhere to put &lt;code&gt;internal-greeter-0.1.0.whl&lt;/code&gt;, and &lt;a href=&#34;https://github.com/pypa/bandersnatch&#34;target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;bandersnatch&lt;/a&gt; for full PyPI mirrors on networks that cannot reach &lt;code&gt;pypi.org&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Hosting an index is the server-side counterpart to &lt;a href=&#34;https://pydevtools.com/handbook/how-to/how-to-use-private-package-indexes-with-uv/&#34;&gt;How to use private package indexes with uv&lt;/a&gt;, which covers the client configuration for managed services like AWS CodeArtifact and JFrog Artifactory.&lt;/p&gt;</description>
    </item>
    <item>
      <title>How to pin GitHub Actions by SHA for Python projects</title>
      <link>https://pydevtools.com/handbook/how-to/how-to-pin-github-actions-by-sha-for-python-projects/</link>
      <pubDate>Wed, 13 May 2026 06:43:22 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/how-to/how-to-pin-github-actions-by-sha-for-python-projects/</guid>
      <description>&lt;p&gt;Tag-based pins like &lt;code&gt;@v7&lt;/code&gt; are mutable references that the action&amp;rsquo;s maintainer (or anyone who compromises their account) can repoint at malicious code, which then runs with access to your CI secrets. The &lt;a href=&#34;https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised&#34;target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;March 2025 tj-actions/changed-files compromise&lt;/a&gt; leaked secrets from thousands of repositories precisely this way. Pinning every third-party action to a full 40-character commit SHA makes the pin immutable and cuts off that attack path. This is the same principle as &lt;a href=&#34;https://pydevtools.com/handbook/how-to/how-to-pin-dependencies-with-hashes-in-uv/&#34;&gt;pinning Python dependencies with hashes&lt;/a&gt;, applied one layer up to your CI.&lt;/p&gt;</description>
    </item>
    <item>
      <title>How to Protect Against Python Supply Chain Attacks with uv</title>
      <link>https://pydevtools.com/handbook/how-to/how-to-protect-against-python-supply-chain-attacks-with-uv/</link>
      <pubDate>Wed, 13 May 2026 06:43:22 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/how-to/how-to-protect-against-python-supply-chain-attacks-with-uv/</guid>
      <description>&lt;p&gt;Malicious packages uploaded to PyPI are usually caught and yanked within hours or days. A dependency cooldown tells &lt;a href=&#34;https://pydevtools.com/handbook/reference/uv/&#34;&gt;uv&lt;/a&gt; to ignore packages published within a recent window, so compromised versions get removed before they ever reach your environment.&lt;/p&gt;
&lt;h2&gt;Set a dependency cooldown&lt;span class=&#34;hx:absolute hx:-mt-20&#34; id=&#34;set-a-dependency-cooldown&#34;&gt;&lt;/span&gt;
    &lt;a href=&#34;#set-a-dependency-cooldown&#34; class=&#34;subheading-anchor&#34; aria-label=&#34;Permalink for this section&#34;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;exclude-newer&lt;/code&gt; setting accepts durations and timestamps. For security hardening, durations are the right choice because they create a rolling window that moves forward with the current date.&lt;/p&gt;</description>
    </item>
    <item>
      <title>How to Publish Python Packages with Digital Attestations</title>
      <link>https://pydevtools.com/handbook/how-to/how-to-publish-python-packages-with-digital-attestations/</link>
      <pubDate>Wed, 13 May 2026 06:43:22 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/how-to/how-to-publish-python-packages-with-digital-attestations/</guid>
      <description>&lt;p&gt;Digital attestations let PyPI record &lt;em&gt;who published&lt;/em&gt; each file, not just what the file contains. When consumers generate a &lt;a href=&#34;https://pydevtools.com/handbook/explanation/what-is-pep-751/&#34;&gt;pylock.toml&lt;/a&gt; lockfile, their tools can write your package&amp;rsquo;s publisher identity into the lockfile. Any change to that identity in a future update becomes visible in code review.&lt;/p&gt;
&lt;p&gt;This guide shows how to configure a GitHub Actions workflow that publishes to PyPI with attestations enabled. The setup requires &lt;a href=&#34;https://pydevtools.com/handbook/explanation/why-use-trusted-publishing-for-pypi/&#34;&gt;trusted publishing&lt;/a&gt;; attestations are cryptographically tied to the same OIDC identity that trusted publishing uses.&lt;/p&gt;</description>
    </item>
    <item>
      <title>How to Publish to PyPI with Trusted Publishing</title>
      <link>https://pydevtools.com/handbook/how-to/how-to-publish-to-pypi-with-trusted-publishing/</link>
      <pubDate>Wed, 13 May 2026 06:43:22 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/how-to/how-to-publish-to-pypi-with-trusted-publishing/</guid>
      <description>&lt;p&gt;Trusted publishing lets GitHub Actions upload packages to &lt;a href=&#34;https://pydevtools.com/handbook/explanation/what-is-pypi/&#34;&gt;PyPI&lt;/a&gt; without storing any secrets. Instead of creating an API token and pasting it into your CI settings, you tell PyPI which GitHub repository and workflow are allowed to publish. GitHub Actions then proves its identity to PyPI using an OIDC token that lives only for the duration of the workflow run.&lt;/p&gt;
&lt;p&gt;This guide covers GitHub Actions, the most common CI provider for Python projects. PyPI also supports trusted publishing from GitLab CI/CD, Google Cloud, and ActiveState.&lt;/p&gt;</description>
    </item>
    <item>
      <title>How to Scan Python Dependencies for Vulnerabilities</title>
      <link>https://pydevtools.com/handbook/how-to/how-to-scan-python-dependencies-for-vulnerabilities/</link>
      <pubDate>Wed, 13 May 2026 06:43:22 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/how-to/how-to-scan-python-dependencies-for-vulnerabilities/</guid>
      <description>&lt;p&gt;Every dependency in a Python project is a potential source of known security vulnerabilities. Scanning those dependencies against a vulnerability database catches problems before they reach production.&lt;/p&gt;
&lt;h2&gt;Using uv audit&lt;span class=&#34;hx:absolute hx:-mt-20&#34; id=&#34;using-uv-audit&#34;&gt;&lt;/span&gt;
    &lt;a href=&#34;#using-uv-audit&#34; class=&#34;subheading-anchor&#34; aria-label=&#34;Permalink for this section&#34;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://pydevtools.com/handbook/reference/uv/&#34;&gt;uv&lt;/a&gt; 0.10.12 and later includes the &lt;code&gt;uv audit&lt;/code&gt; command, which checks project dependencies against the &lt;a href=&#34;https://osv.dev/&#34;target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;OSV&lt;/a&gt; (Open Source Vulnerabilities) database.&lt;/p&gt;
&lt;p&gt;Run it from the root of a uv project:&lt;/p&gt;
&lt;div class=&#34;hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code&#34;&gt;

&lt;div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-console&#34; data-lang=&#34;console&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gp&#34;&gt;$&lt;/span&gt; uv audit
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&#34;hextra-code-copy-btn-container  hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0&#34;&gt;
  &lt;button
    class=&#34;hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50&#34;
    title=&#34;Copy code&#34;
    aria-label=&#34;Copy code&#34;
    data-copied-label=&#34;Copied!&#34;
  &gt;
    &lt;div class=&#34;hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4&#34;&gt;&lt;/div&gt;
&lt;div class=&#34;hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4&#34;&gt;&lt;/div&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;uv audit&lt;/code&gt; reads the project&amp;rsquo;s lockfile and queries OSV for known vulnerabilities in each dependency. When vulnerabilities are found, it prints details with links to the relevant advisories and exits with a non-zero status code. When no vulnerabilities are found, it exits with status 0.&lt;/p&gt;</description>
    </item>
    <item>
      <title>How to upgrade setup-uv from v7 to v8</title>
      <link>https://pydevtools.com/handbook/how-to/how-to-upgrade-setup-uv-from-v7-to-v8/</link>
      <pubDate>Wed, 13 May 2026 06:43:22 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/how-to/how-to-upgrade-setup-uv-from-v7-to-v8/</guid>
      <description>&lt;p&gt;If a GitHub Actions workflow uses &lt;a href=&#34;https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv/&#34;&gt;&lt;code&gt;astral-sh/setup-uv&lt;/code&gt;&lt;/a&gt;&lt;code&gt;@v7&lt;/code&gt;, Dependabot will not bump it to v8 automatically, and &lt;code&gt;@v8&lt;/code&gt; no longer resolves. Here is the fix.&lt;/p&gt;
&lt;h2&gt;What changed in v8.0.0&lt;span class=&#34;hx:absolute hx:-mt-20&#34; id=&#34;what-changed-in-v800&#34;&gt;&lt;/span&gt;
    &lt;a href=&#34;#what-changed-in-v800&#34; class=&#34;subheading-anchor&#34; aria-label=&#34;Permalink for this section&#34;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Version 8.0.0 is the first immutable release of &lt;code&gt;setup-uv&lt;/code&gt;, published in March 2026. Moving tags are gone: &lt;code&gt;@v8&lt;/code&gt; and &lt;code&gt;@v8.0&lt;/code&gt; do not exist, and no major or minor tag will ever be published again. Only full-version tags such as &lt;code&gt;@v8.0.0&lt;/code&gt; resolve, and each one is immutable once cut. The change follows GitHub&amp;rsquo;s supply-chain guidance for actions, limiting the blast radius if a maintainer account is compromised (see the &lt;a href=&#34;https://github.com/astral-sh/setup-uv/releases/tag/v8.0.0&#34;target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;v8.0.0 release notes&lt;/a&gt;).&lt;/p&gt;</description>
    </item>
    <item>
      <title>How to Verify Dependencies with Hashes in uv</title>
      <link>https://pydevtools.com/handbook/how-to/how-to-pin-dependencies-with-hashes-in-uv/</link>
      <pubDate>Wed, 13 May 2026 06:43:22 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/how-to/how-to-pin-dependencies-with-hashes-in-uv/</guid>
      <description>&lt;p&gt;Dependency hashes let &lt;a href=&#34;https://pydevtools.com/handbook/reference/uv/&#34;&gt;uv&lt;/a&gt; verify that every downloaded artifact matches what was resolved, catching tampering before anything gets installed.&lt;/p&gt;
&lt;h2&gt;Hashes in uv.lock&lt;span class=&#34;hx:absolute hx:-mt-20&#34; id=&#34;hashes-in-uvlock&#34;&gt;&lt;/span&gt;
    &lt;a href=&#34;#hashes-in-uvlock&#34; class=&#34;subheading-anchor&#34; aria-label=&#34;Permalink for this section&#34;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;uv.lock&lt;/code&gt; includes SHA-256 hashes for every distribution by default. After running &lt;code&gt;uv lock&lt;/code&gt; or &lt;code&gt;uv add&lt;/code&gt;, each entry in the lockfile contains hashes for both sdists and wheels:&lt;/p&gt;
&lt;div class=&#34;hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code&#34;&gt;

&lt;div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;package&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;certifi&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;version&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;2026.2.25&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;source&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;registry&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://pypi.org/simple&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;sdist&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;url&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;...&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;hash&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;wheels&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;url&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;...&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;hash&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&#34;hextra-code-copy-btn-container  hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0&#34;&gt;
  &lt;button
    class=&#34;hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50&#34;
    title=&#34;Copy code&#34;
    aria-label=&#34;Copy code&#34;
    data-copied-label=&#34;Copied!&#34;
  &gt;
    &lt;div class=&#34;hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4&#34;&gt;&lt;/div&gt;
&lt;div class=&#34;hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4&#34;&gt;&lt;/div&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;When &lt;code&gt;uv sync&lt;/code&gt; or &lt;code&gt;uv run&lt;/code&gt; installs packages, it checks each downloaded artifact against these hashes. If a hash does not match, installation fails.&lt;/p&gt;</description>
    </item>
    <item>
      <title>How to Vet a Python Package Before Installing It</title>
      <link>https://pydevtools.com/handbook/how-to/how-to-vet-a-python-package-before-installing-it/</link>
      <pubDate>Fri, 15 May 2026 21:28:17 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/how-to/how-to-vet-a-python-package-before-installing-it/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://pydevtools.com/blog/litellm-supply-chain-attack-and-securing-python-dependencies/&#34;&gt;litellm 1.82.7 and 1.82.8&lt;/a&gt;, and &lt;a href=&#34;https://pydevtools.com/blog/lightning-pypi-compromise-import-time-supply-chain-attack/&#34;&gt;Lightning AI 2.6.2 and 2.6.3&lt;/a&gt;, hit PyPI before matching source releases appeared on GitHub. A 30-second tag check would have caught either one. Use this five-minute pre-install routine to catch that kind of &lt;a href=&#34;https://pydevtools.com/handbook/explanation/what-is-a-python-supply-chain-attack/&#34;&gt;Python supply chain attack&lt;/a&gt; before adding a package to &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Read the PyPI signals&lt;span class=&#34;hx:absolute hx:-mt-20&#34; id=&#34;read-the-pypi-signals&#34;&gt;&lt;/span&gt;
    &lt;a href=&#34;#read-the-pypi-signals&#34; class=&#34;subheading-anchor&#34; aria-label=&#34;Permalink for this section&#34;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Open the package page on &lt;a href=&#34;https://pydevtools.com/handbook/explanation/what-is-pypi/&#34;&gt;PyPI&lt;/a&gt; and check four things.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Download trend.&lt;/strong&gt; Run &lt;a href=&#34;https://pydevtools.com/handbook/reference/uvx/&#34;&gt;&lt;code&gt;uvx&lt;/code&gt;&lt;/a&gt; with &lt;a href=&#34;https://github.com/hugovk/pypistats&#34;target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;pypistats&lt;/code&gt;&lt;/a&gt; to see recent download counts:&lt;/p&gt;</description>
    </item>
    <item>
      <title>What Is a Python Supply Chain Attack?</title>
      <link>https://pydevtools.com/handbook/explanation/what-is-a-python-supply-chain-attack/</link>
      <pubDate>Mon, 11 May 2026 07:22:13 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/explanation/what-is-a-python-supply-chain-attack/</guid>
      <description>&lt;p&gt;A Python supply chain attack is a class of compromise where the attacker reaches your project not by breaking your code, but by tampering with something your project trusts. The trusted thing might be a package on &lt;a href=&#34;https://pydevtools.com/handbook/explanation/what-is-pypi/&#34;&gt;PyPI&lt;/a&gt;, a maintainer account, a build pipeline, a transitive dependency, or the registry itself. By the time &lt;code&gt;uv sync&lt;/code&gt; or &lt;code&gt;pip install&lt;/code&gt; runs, the malicious code is already inside the dependency graph and the install treats it like any other package.&lt;/p&gt;</description>
    </item>
    <item>
      <title>What is PEP 829?</title>
      <link>https://pydevtools.com/handbook/explanation/what-is-pep-829/</link>
      <pubDate>Thu, 07 May 2026 13:56:14 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/explanation/what-is-pep-829/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://peps.python.org/pep-0829/&#34;target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;PEP 829: Package Startup Configuration Files&lt;/a&gt; splits the two jobs that &lt;code&gt;.pth&lt;/code&gt; files do today into two separate files. &lt;code&gt;.pth&lt;/code&gt; keeps the &lt;code&gt;sys.path&lt;/code&gt; extension job. A new &lt;code&gt;&amp;lt;name&amp;gt;.start&lt;/code&gt; file takes over package initialization, replacing the &lt;code&gt;import&lt;/code&gt; line that turns every &lt;code&gt;.pth&lt;/code&gt; file into an &lt;code&gt;exec()&lt;/code&gt; surface at interpreter startup. Authored by Barry Warsaw and accepted on 24 April 2026 for Python 3.15. Python 3.15 reached beta 1 on 7 May 2026, the first build to ship the &lt;code&gt;.start&lt;/code&gt; mechanism for package authors to test against.&lt;/p&gt;</description>
    </item>
    <item>
      <title>What is PyPI (Python Package Index)?</title>
      <link>https://pydevtools.com/handbook/explanation/what-is-pypi/</link>
      <pubDate>Fri, 15 May 2026 07:00:57 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/explanation/what-is-pypi/</guid>
      <description>&lt;p&gt;PyPI (Python Package Index) is the official package repository for Python. When a developer runs &lt;code&gt;uv add requests&lt;/code&gt; or &lt;code&gt;pip install requests&lt;/code&gt;, the package is downloaded from PyPI by default. It hosts over 600,000 projects and serves billions of downloads per month.&lt;/p&gt;
&lt;p&gt;PyPI is maintained by the &lt;a href=&#34;https://pydevtools.com/handbook/explanation/what-is-pypa/&#34;&gt;Python Packaging Authority (PyPA)&lt;/a&gt; and powered by an open-source application called &lt;a href=&#34;https://warehouse.pypa.io&#34;target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;Warehouse&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What PyPI hosts&lt;span class=&#34;hx:absolute hx:-mt-20&#34; id=&#34;what-pypi-hosts&#34;&gt;&lt;/span&gt;
    &lt;a href=&#34;#what-pypi-hosts&#34; class=&#34;subheading-anchor&#34; aria-label=&#34;Permalink for this section&#34;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;PyPI stores two types of &lt;a href=&#34;https://pydevtools.com/handbook/explanation/what-is-a-python-package/&#34;&gt;distribution packages&lt;/a&gt;:&lt;/p&gt;</description>
    </item>
    <item>
      <title>Why Installing a Python Package Can Run Code</title>
      <link>https://pydevtools.com/handbook/explanation/why-installing-a-python-package-can-run-code/</link>
      <pubDate>Fri, 01 May 2026 14:28:04 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/explanation/why-installing-a-python-package-can-run-code/</guid>
      <description>&lt;p&gt;&lt;code&gt;pip install&lt;/code&gt; has never meant &amp;ldquo;copy some files into site-packages.&amp;rdquo; A Python package gets four separate chances to run code on your machine: once when it installs, once whenever it gets imported, and twice more every time you start Python afterward.&lt;/p&gt;
&lt;p&gt;In March 2026, a malicious release of &lt;a href=&#34;https://pydevtools.com/blog/litellm-supply-chain-attack-and-securing-python-dependencies/&#34;&gt;litellm&lt;/a&gt; exploited one of them. The payload was a &lt;code&gt;.pth&lt;/code&gt; file that ran on every Python invocation and quietly sent cloud credentials and Kubernetes secrets to an attacker-controlled server. Six weeks later, the &lt;a href=&#34;https://pydevtools.com/blog/lightning-pypi-compromise-import-time-supply-chain-attack/&#34;&gt;lightning compromise&lt;/a&gt; used a different surface: a daemon thread spawned from the package&amp;rsquo;s &lt;code&gt;__init__.py&lt;/code&gt; on import. The other surfaces are just as available, and the handbook&amp;rsquo;s supply-chain defenses only make sense once you can see all four.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Why pylock.toml Includes Digital Attestations</title>
      <link>https://pydevtools.com/handbook/explanation/why-pylock-toml-includes-digital-attestations/</link>
      <pubDate>Tue, 28 Apr 2026 21:54:51 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/explanation/why-pylock-toml-includes-digital-attestations/</guid>
      <description>&lt;p&gt;A &lt;a href=&#34;https://pydevtools.com/handbook/explanation/what-is-a-lock-file/&#34;&gt;lockfile&lt;/a&gt; pins exact versions. Hashes verify that the files you download match the files the resolver saw. But neither tells you &lt;em&gt;who published&lt;/em&gt; those files. That gap is what digital attestations in &lt;a href=&#34;https://pydevtools.com/handbook/explanation/what-is-pep-751/&#34;&gt;pylock.toml&lt;/a&gt; close.&lt;/p&gt;
&lt;h2&gt;The gap between hashes and provenance&lt;span class=&#34;hx:absolute hx:-mt-20&#34; id=&#34;the-gap-between-hashes-and-provenance&#34;&gt;&lt;/span&gt;
    &lt;a href=&#34;#the-gap-between-hashes-and-provenance&#34; class=&#34;subheading-anchor&#34; aria-label=&#34;Permalink for this section&#34;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Hashes prove integrity: the file has not been altered. They do not prove origin. If an attacker gains upload access to &lt;a href=&#34;https://pydevtools.com/handbook/explanation/what-is-pypi/&#34;&gt;PyPI&lt;/a&gt; and publishes a new release of a package you depend on, the new release will have valid hashes. Your lockfile will accept them during the next update because the file matches itself. Nothing looks wrong.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Why Use Trusted Publishing for PyPI?</title>
      <link>https://pydevtools.com/handbook/explanation/why-use-trusted-publishing-for-pypi/</link>
      <pubDate>Tue, 28 Apr 2026 21:54:51 -0400</pubDate>
      <author>Tim Hopper</author>
      <guid>https://pydevtools.com/handbook/explanation/why-use-trusted-publishing-for-pypi/</guid>
      <description>&lt;p&gt;Most Python packages reach &lt;a href=&#34;https://pydevtools.com/handbook/explanation/what-is-pypi/&#34;&gt;PyPI&lt;/a&gt; through a CI pipeline. A GitHub Actions workflow builds the package, then uploads it using an API token stored as a repository secret. That token is the weak link: it never expires, it works from any machine, and anyone who obtains it can upload whatever they want to &lt;a href=&#34;https://pypi.org&#34;target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;PyPI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Trusted publishing replaces that stored token with short-lived, CI-scoped credentials. Instead of &amp;ldquo;does this request have a valid secret?&amp;rdquo;, PyPI asks &amp;ldquo;does this request come from an authorized CI workflow?&amp;rdquo; The result: there is no long-lived secret to steal.&lt;/p&gt;</description>
    </item>
    
  </channel>
</rss>
