diff --git a/lib/versioning/poetry/index.spec.ts b/lib/versioning/poetry/index.spec.ts
index 6f9d3a999e5794cde237177955718e2c4b207964..9f835ebb6bbf069222fc1a449ba956fc0e81b73a 100644
--- a/lib/versioning/poetry/index.spec.ts
+++ b/lib/versioning/poetry/index.spec.ts
@@ -3,25 +3,33 @@ import { api as versioning } from '.';
 describe('versioning/poetry/index', () => {
   describe('equals', () => {
     test.each`
-      a             | b        | expected
-      ${'1'}        | ${'1'}   | ${true}
-      ${'1.0'}      | ${'1'}   | ${true}
-      ${'1.0.0'}    | ${'1'}   | ${true}
-      ${'1.9.0'}    | ${'1.9'} | ${true}
-      ${'1'}        | ${'2'}   | ${false}
-      ${'1.9.1'}    | ${'1.9'} | ${false}
-      ${'1.9-beta'} | ${'1.9'} | ${false}
+      a               | b                 | expected
+      ${'1'}          | ${'1'}            | ${true}
+      ${'1.0'}        | ${'1'}            | ${true}
+      ${'1.0.0'}      | ${'1'}            | ${true}
+      ${'1.9.0'}      | ${'1.9'}          | ${true}
+      ${'1'}          | ${'2'}            | ${false}
+      ${'1.9.1'}      | ${'1.9'}          | ${false}
+      ${'1.9-beta'}   | ${'1.9'}          | ${false}
+      ${'1.9b0'}      | ${'1.9'}          | ${false}
+      ${'1.9b0'}      | ${'1.9.0-beta.0'} | ${true}
+      ${'1.9-0'}      | ${'1.9.0-post.0'} | ${true}
+      ${'1.9.0-post'} | ${'1.9.0-post.0'} | ${true}
+      ${'1.9.0dev0'}  | ${'1.9.0-dev.0'}  | ${true}
     `('equals("$a", "$b") === $expected', ({ a, b, expected }) => {
       expect(versioning.equals(a, b)).toBe(expected);
     });
   });
 
   test.each`
-    version    | major | minor | patch
-    ${'1'}     | ${1}  | ${0}  | ${0}
-    ${'1.9'}   | ${1}  | ${9}  | ${0}
-    ${'1.9.0'} | ${1}  | ${9}  | ${0}
-    ${'1.9.4'} | ${1}  | ${9}  | ${4}
+    version          | major | minor | patch
+    ${'1'}           | ${1}  | ${0}  | ${0}
+    ${'1.9'}         | ${1}  | ${9}  | ${0}
+    ${'1.9.0'}       | ${1}  | ${9}  | ${0}
+    ${'1.9.4'}       | ${1}  | ${9}  | ${4}
+    ${'1.9.4b0'}     | ${1}  | ${9}  | ${4}
+    ${'1.9.4-beta0'} | ${1}  | ${9}  | ${4}
+    ${'17.04.01'}    | ${17} | ${4}  | ${1}
   `(
     'getMajor, getMinor, getPatch for "$version"',
     ({ version, major, minor, patch }) => {
@@ -38,6 +46,7 @@ describe('versioning/poetry/index', () => {
     ${'2.0.0'}  | ${'1'}        | ${true}
     ${'1.10.0'} | ${'1.9'}      | ${true}
     ${'1.9'}    | ${'1.9-beta'} | ${true}
+    ${'1.9'}    | ${'1.9a0'}    | ${true}
     ${'1'}      | ${'1'}        | ${false}
     ${'1.0'}    | ${'1'}        | ${false}
     ${'1.0.0'}  | ${'1'}        | ${false}
@@ -53,21 +62,41 @@ describe('versioning/poetry/index', () => {
     ${'1.9.0'}      | ${true}
     ${'1.9.4'}      | ${true}
     ${'1.9.4-beta'} | ${false}
+    ${'1.9.4a0'}    | ${false}
   `('isStable("$version") === $expected', ({ version, expected }) => {
     const res = !!versioning.isStable(version);
     expect(res).toBe(expected);
   });
 
+  test.each`
+    version        | expected
+    ${'1.2.3a0'}   | ${true}
+    ${'1.2.3b1'}   | ${true}
+    ${'1.2.3rc23'} | ${true}
+    ${'17.04.01'}  | ${true}
+    ${'17.b4.0'}   | ${false}
+    ${'0.98.5.1'}  | ${false}
+  `('isVersion("$version") === $expected', ({ version, expected }) => {
+    expect(!!versioning.isVersion(version)).toBe(expected);
+  });
+
   test.each`
     version                                          | expected
-    ${'17.04.0'}                                     | ${false}
+    ${'17.04.00'}                                    | ${true}
+    ${'17.b4.0'}                                     | ${false}
     ${'1.2.3'}                                       | ${true}
     ${'1.2.3-foo'}                                   | ${true}
     ${'1.2.3foo'}                                    | ${false}
+    ${'1.2.3a0'}                                     | ${true}
+    ${'1.2.3b1'}                                     | ${true}
+    ${'1.2.3rc23'}                                   | ${true}
     ${'*'}                                           | ${true}
     ${'~1.2.3'}                                      | ${true}
     ${'^1.2.3'}                                      | ${true}
     ${'>1.2.3'}                                      | ${true}
+    ${'~=1.9'}                                       | ${true}
+    ${'==1.9'}                                       | ${true}
+    ${'===1.9.4'}                                    | ${true}
     ${'renovatebot/renovate'}                        | ${false}
     ${'renovatebot/renovate#master'}                 | ${false}
     ${'https://github.com/renovatebot/renovate.git'} | ${false}
@@ -87,15 +116,20 @@ describe('versioning/poetry/index', () => {
   });
 
   test.each`
-    version    | range                     | expected
-    ${'4.2.0'} | ${'4.2, >= 3.0, < 5.0.0'} | ${true}
-    ${'4.2.0'} | ${'2.0, >= 3.0, < 5.0.0'} | ${false}
-    ${'4.2.2'} | ${'4.2.0, < 4.2.4'}       | ${false}
-    ${'4.2.2'} | ${'^4.2.0, < 4.2.4'}      | ${true}
-    ${'4.2.0'} | ${'4.3.0, 3.0.0'}         | ${false}
-    ${'4.2.0'} | ${'> 5.0.0, <= 6.0.0'}    | ${false}
-    ${'4.2.0'} | ${'*'}                    | ${true}
-    ${'1.4'}   | ${'1.4'}                  | ${true}
+    version      | range                     | expected
+    ${'4.2.0'}   | ${'4.2, >= 3.0, < 5.0.0'} | ${true}
+    ${'4.2.0'}   | ${'2.0, >= 3.0, < 5.0.0'} | ${false}
+    ${'4.2.2'}   | ${'4.2.0, < 4.2.4'}       | ${false}
+    ${'4.2.2'}   | ${'^4.2.0, < 4.2.4'}      | ${true}
+    ${'4.2.0'}   | ${'4.3.0, 3.0.0'}         | ${false}
+    ${'4.2.0'}   | ${'> 5.0.0, <= 6.0.0'}    | ${false}
+    ${'4.2.0'}   | ${'*'}                    | ${true}
+    ${'1.9.4'}   | ${'==1.9'}                | ${true}
+    ${'1.9.4'}   | ${'===1.9.4'}             | ${true}
+    ${'1.9.4'}   | ${'===1.9.3'}             | ${false}
+    ${'0.8.0a1'} | ${'^0.8.0-alpha.0'}       | ${true}
+    ${'0.7.4'}   | ${'^0.8.0-alpha.0'}       | ${false}
+    ${'1.4'}     | ${'1.4'}                  | ${true}
   `(
     'matches("$version", "$range") === "$expected"',
     ({ version, range, expected }) => {
@@ -121,6 +155,7 @@ describe('versioning/poetry/index', () => {
     ${['0.4.0', '0.5.0', '4.2.0', '5.0.0']}          | ${'^4.0.0, = 0.5.0'}           | ${null}
     ${['0.4.0', '0.5.0', '4.2.0', '5.0.0']}          | ${'^4.0.0, > 4.1.0, <= 4.3.5'} | ${'4.2.0'}
     ${['0.4.0', '0.5.0', '4.2.0', '5.0.0']}          | ${'^6.2.0, 3.*'}               | ${null}
+    ${['0.8.0a2', '0.8.0a7']}                        | ${'^0.8.0-alpha.0'}            | ${'0.8.0-alpha.2'}
   `(
     'minSatisfyingVersion($versions, "$range") === $expected',
     ({ versions, range, expected }) => {
@@ -132,6 +167,7 @@ describe('versioning/poetry/index', () => {
     versions                                                  | range               | expected
     ${['4.2.1', '0.4.0', '0.5.0', '4.0.0', '4.2.0', '5.0.0']} | ${'4.*.0, < 4.2.5'} | ${'4.2.1'}
     ${['0.4.0', '0.5.0', '4.0.0', '4.2.0', '5.0.0', '5.0.3']} | ${'5.0, > 5.0.0'}   | ${'5.0.3'}
+    ${['0.8.0a2', '0.8.0a7']}                                 | ${'^0.8.0-alpha.0'} | ${'0.8.0-alpha.7'}
   `(
     'getSatisfyingVersion($versions, "$range") === $expected',
     ({ versions, range, expected }) => {
@@ -139,48 +175,53 @@ describe('versioning/poetry/index', () => {
     }
   );
   test.each`
-    currentValue     | rangeStrategy | currentVersion | newVersion              | expected
-    ${'1.0.0'}       | ${'bump'}     | ${'1.0.0'}     | ${'1.1.0'}              | ${'1.1.0'}
-    ${'   1.0.0'}    | ${'bump'}     | ${'1.0.0'}     | ${'1.1.0'}              | ${'1.1.0'}
-    ${'1.0.0'}       | ${'bump'}     | ${'1.0.0'}     | ${'1.1.0'}              | ${'1.1.0'}
-    ${'=1.0.0'}      | ${'bump'}     | ${'1.0.0'}     | ${'1.1.0'}              | ${'=1.1.0'}
-    ${'=  1.0.0'}    | ${'bump'}     | ${'1.0.0'}     | ${'1.1.0'}              | ${'=1.1.0'}
-    ${'= 1.0.0'}     | ${'bump'}     | ${'1.0.0'}     | ${'1.1.0'}              | ${'=1.1.0'}
-    ${'  = 1.0.0'}   | ${'bump'}     | ${'1.0.0'}     | ${'1.1.0'}              | ${'=1.1.0'}
-    ${'  =   1.0.0'} | ${'bump'}     | ${'1.0.0'}     | ${'1.1.0'}              | ${'=1.1.0'}
-    ${'=    1.0.0'}  | ${'bump'}     | ${'1.0.0'}     | ${'1.1.0'}              | ${'=1.1.0'}
-    ${'^1.0'}        | ${'bump'}     | ${'1.0.0'}     | ${'1.0.7'}              | ${'^1.0'}
-    ${'^1.0.0'}      | ${'replace'}  | ${'1.0.0'}     | ${'2.0.7'}              | ${'^2.0.0'}
-    ${'^5.0.3'}      | ${'replace'}  | ${'5.3.1'}     | ${'5.5'}                | ${'^5.0.3'}
-    ${'1.0.0'}       | ${'replace'}  | ${'1.0.0'}     | ${'2.0.7'}              | ${'2.0.7'}
-    ${'^1.0.0'}      | ${'replace'}  | ${'1.0.0'}     | ${'2.0.7'}              | ${'^2.0.0'}
-    ${'^0.5.15'}     | ${'replace'}  | ${'0.5.15'}    | ${'0.6'}                | ${'^0.5.15'}
-    ${'^1'}          | ${'bump'}     | ${'1.0.0'}     | ${'2.1.7'}              | ${'^2'}
-    ${'~1'}          | ${'bump'}     | ${'1.0.0'}     | ${'1.1.7'}              | ${'~1'}
-    ${'5'}           | ${'bump'}     | ${'5.0.0'}     | ${'5.1.7'}              | ${'5'}
-    ${'5'}           | ${'bump'}     | ${'5.0.0'}     | ${'6.1.7'}              | ${'6'}
-    ${'5.0'}         | ${'bump'}     | ${'5.0.0'}     | ${'5.0.7'}              | ${'5.0'}
-    ${'5.0'}         | ${'bump'}     | ${'5.0.0'}     | ${'5.1.7'}              | ${'5.1'}
-    ${'5.0'}         | ${'bump'}     | ${'5.0.0'}     | ${'6.1.7'}              | ${'6.1'}
-    ${'5.0'}         | ${'replace'}  | ${'5.0.0'}     | ${'6.1.7'}              | ${'6.1'}
-    ${'=1.0.0'}      | ${'replace'}  | ${'1.0.0'}     | ${'1.1.0'}              | ${'=1.1.0'}
-    ${'^1'}          | ${'bump'}     | ${'1.0.0'}     | ${'1.0.7-prerelease.1'} | ${'^1.0.7-prerelease.1'}
-    ${'^1.0.0'}      | ${'replace'}  | ${'1.0.0'}     | ${'1.2.3'}              | ${'^1.0.0'}
-    ${'~1.0'}        | ${'bump'}     | ${'1.0.0'}     | ${'1.1.7'}              | ${'~1.1'}
-    ${'1.0.*'}       | ${'replace'}  | ${'1.0.0'}     | ${'1.1.0'}              | ${'1.1.*'}
-    ${'1.*'}         | ${'replace'}  | ${'1.0.0'}     | ${'2.1.0'}              | ${'2.*'}
-    ${'~0.6.1'}      | ${'replace'}  | ${'0.6.8'}     | ${'0.7.0-rc.2'}         | ${'~0.7.0-rc'}
-    ${'<1.3.4'}      | ${'replace'}  | ${'1.2.3'}     | ${'1.5.0'}              | ${'<1.5.1'}
-    ${'< 1.3.4'}     | ${'replace'}  | ${'1.2.3'}     | ${'1.5.0'}              | ${'< 1.5.1'}
-    ${'<   1.3.4'}   | ${'replace'}  | ${'1.2.3'}     | ${'1.5.0'}              | ${'< 1.5.1'}
-    ${'<=1.3.4'}     | ${'replace'}  | ${'1.2.3'}     | ${'1.5.0'}              | ${'<=1.5.0'}
-    ${'<= 1.3.4'}    | ${'replace'}  | ${'1.2.3'}     | ${'1.5.0'}              | ${'<= 1.5.0'}
-    ${'<=   1.3.4'}  | ${'replace'}  | ${'1.2.3'}     | ${'1.5.0'}              | ${'<= 1.5.0'}
-    ${'^1.2'}        | ${'replace'}  | ${'1.2.3'}     | ${'2.0.0'}              | ${'^2.0'}
-    ${'^1'}          | ${'replace'}  | ${'1.2.3'}     | ${'2.0.0'}              | ${'^2'}
-    ${'~1.2'}        | ${'replace'}  | ${'1.2.3'}     | ${'2.0.0'}              | ${'~2.0'}
-    ${'~1'}          | ${'replace'}  | ${'1.2.3'}     | ${'2.0.0'}              | ${'~2'}
-    ${'^2.2'}        | ${'widen'}    | ${'2.2.0'}     | ${'3.0.0'}              | ${'^2.2 || ^3.0.0'}
+    currentValue        | rangeStrategy | currentVersion     | newVersion         | expected
+    ${'1.0.0'}          | ${'bump'}     | ${'1.0.0'}         | ${'1.1.0'}         | ${'1.1.0'}
+    ${'   1.0.0'}       | ${'bump'}     | ${'1.0.0'}         | ${'1.1.0'}         | ${'1.1.0'}
+    ${'1.0.0'}          | ${'bump'}     | ${'1.0.0'}         | ${'1.1.0'}         | ${'1.1.0'}
+    ${'=1.0.0'}         | ${'bump'}     | ${'1.0.0'}         | ${'1.1.0'}         | ${'=1.1.0'}
+    ${'=  1.0.0'}       | ${'bump'}     | ${'1.0.0'}         | ${'1.1.0'}         | ${'=1.1.0'}
+    ${'= 1.0.0'}        | ${'bump'}     | ${'1.0.0'}         | ${'1.1.0'}         | ${'=1.1.0'}
+    ${'  = 1.0.0'}      | ${'bump'}     | ${'1.0.0'}         | ${'1.1.0'}         | ${'=1.1.0'}
+    ${'  =   1.0.0'}    | ${'bump'}     | ${'1.0.0'}         | ${'1.1.0'}         | ${'=1.1.0'}
+    ${'=    1.0.0'}     | ${'bump'}     | ${'1.0.0'}         | ${'1.1.0'}         | ${'=1.1.0'}
+    ${'^1.0'}           | ${'bump'}     | ${'1.0.0'}         | ${'1.0.7'}         | ${'^1.0'}
+    ${'^1.0.0'}         | ${'replace'}  | ${'1.0.0'}         | ${'2.0.7'}         | ${'^2.0.0'}
+    ${'^5.0.3'}         | ${'replace'}  | ${'5.3.1'}         | ${'5.5'}           | ${'^5.0.3'}
+    ${'1.0.0'}          | ${'replace'}  | ${'1.0.0'}         | ${'2.0.7'}         | ${'2.0.7'}
+    ${'^1.0.0'}         | ${'replace'}  | ${'1.0.0'}         | ${'2.0.7'}         | ${'^2.0.0'}
+    ${'^0.5.15'}        | ${'replace'}  | ${'0.5.15'}        | ${'0.6'}           | ${'^0.5.15'}
+    ${'^0.5.15'}        | ${'replace'}  | ${'0.5.15'}        | ${'0.6b.4'}        | ${'^0.5.15'}
+    ${'^1'}             | ${'bump'}     | ${'1.0.0'}         | ${'2.1.7'}         | ${'^2'}
+    ${'~1'}             | ${'bump'}     | ${'1.0.0'}         | ${'1.1.7'}         | ${'~1'}
+    ${'5'}              | ${'bump'}     | ${'5.0.0'}         | ${'5.1.7'}         | ${'5'}
+    ${'5'}              | ${'bump'}     | ${'5.0.0'}         | ${'6.1.7'}         | ${'6'}
+    ${'5.0'}            | ${'bump'}     | ${'5.0.0'}         | ${'5.0.7'}         | ${'5.0'}
+    ${'5.0'}            | ${'bump'}     | ${'5.0.0'}         | ${'5.1.7'}         | ${'5.1'}
+    ${'5.0'}            | ${'bump'}     | ${'5.0.0'}         | ${'6.1.7'}         | ${'6.1'}
+    ${'5.0'}            | ${'bump'}     | ${'5.0.0'}         | ${'6.b0.0'}        | ${'5.0'}
+    ${'5.0'}            | ${'replace'}  | ${'5.0.0'}         | ${'6.1.7'}         | ${'6.1'}
+    ${'=1.0.0'}         | ${'replace'}  | ${'1.0.0'}         | ${'1.1.0'}         | ${'=1.1.0'}
+    ${'^1'}             | ${'bump'}     | ${'1.0.0'}         | ${'1.0.7rc.1'}     | ${'^1.0.7-rc.1'}
+    ${'^1'}             | ${'bump'}     | ${'1.0.0'}         | ${'1.0.7a0'}       | ${'^1.0.7-alpha.0'}
+    ${'^0.8.0-alpha.0'} | ${'bump'}     | ${'0.8.0-alpha.0'} | ${'0.8.0-alpha.1'} | ${'^0.8.0-alpha.1'}
+    ${'^0.8.0-alpha.0'} | ${'bump'}     | ${'0.8.0-alpha.0'} | ${'0.8.0a1'}       | ${'^0.8.0-alpha.1'}
+    ${'^1.0.0'}         | ${'replace'}  | ${'1.0.0'}         | ${'1.2.3'}         | ${'^1.0.0'}
+    ${'~1.0'}           | ${'bump'}     | ${'1.0.0'}         | ${'1.1.7'}         | ${'~1.1'}
+    ${'1.0.*'}          | ${'replace'}  | ${'1.0.0'}         | ${'1.1.0'}         | ${'1.1.*'}
+    ${'1.*'}            | ${'replace'}  | ${'1.0.0'}         | ${'2.1.0'}         | ${'2.*'}
+    ${'~0.6.1'}         | ${'replace'}  | ${'0.6.8'}         | ${'0.7.0-rc.2'}    | ${'~0.7.0-rc'}
+    ${'<1.3.4'}         | ${'replace'}  | ${'1.2.3'}         | ${'1.5.0'}         | ${'<1.5.1'}
+    ${'< 1.3.4'}        | ${'replace'}  | ${'1.2.3'}         | ${'1.5.0'}         | ${'< 1.5.1'}
+    ${'<   1.3.4'}      | ${'replace'}  | ${'1.2.3'}         | ${'1.5.0'}         | ${'< 1.5.1'}
+    ${'<=1.3.4'}        | ${'replace'}  | ${'1.2.3'}         | ${'1.5.0'}         | ${'<=1.5.0'}
+    ${'<= 1.3.4'}       | ${'replace'}  | ${'1.2.3'}         | ${'1.5.0'}         | ${'<= 1.5.0'}
+    ${'<=   1.3.4'}     | ${'replace'}  | ${'1.2.3'}         | ${'1.5.0'}         | ${'<= 1.5.0'}
+    ${'^1.2'}           | ${'replace'}  | ${'1.2.3'}         | ${'2.0.0'}         | ${'^2.0'}
+    ${'^1'}             | ${'replace'}  | ${'1.2.3'}         | ${'2.0.0'}         | ${'^2'}
+    ${'~1.2'}           | ${'replace'}  | ${'1.2.3'}         | ${'2.0.0'}         | ${'~2.0'}
+    ${'~1'}             | ${'replace'}  | ${'1.2.3'}         | ${'2.0.0'}         | ${'~2'}
+    ${'^2.2'}           | ${'widen'}    | ${'2.2.0'}         | ${'3.0.0'}         | ${'^2.2 || ^3.0.0'}
   `(
     'getNewValue("$currentValue", "$rangeStrategy", "$currentVersion", "$newVersion") === "$expected"',
     ({ currentValue, rangeStrategy, currentVersion, newVersion, expected }) => {
@@ -205,6 +246,8 @@ describe('versioning/poetry/index', () => {
     ${'1.0'}    | ${'1'}        | ${0}
     ${'1.0.0'}  | ${'1'}        | ${0}
     ${'1.9.0'}  | ${'1.9'}      | ${0}
+    ${'1.9'}    | ${'1.9b'}     | ${1}
+    ${'1.9'}    | ${'1.9rc0'}   | ${1}
   `('sortVersions("$a", "$b") === $expected', ({ a, b, expected }) => {
     expect(versioning.sortVersions(a, b)).toEqual(expected);
   });
diff --git a/lib/versioning/poetry/index.ts b/lib/versioning/poetry/index.ts
index 144c84360ebbc31db466e7152685537aa1ba9218..e2914024d600c4fad31569764e6d64c25079782a 100644
--- a/lib/versioning/poetry/index.ts
+++ b/lib/versioning/poetry/index.ts
@@ -1,8 +1,14 @@
 import { parseRange } from 'semver-utils';
 import { logger } from '../../logger';
 import { api as npm } from '../npm';
-import { api as pep440 } from '../pep440';
 import type { NewValueConfig, VersioningApi } from '../types';
+import { VERSION_PATTERN } from './patterns';
+import {
+  npm2poetry,
+  poetry2npm,
+  poetry2semver,
+  semver2poetry,
+} from './transform';
 
 export const id = 'poetry';
 export const displayName = 'Poetry';
@@ -10,130 +16,76 @@ export const urls = ['https://python-poetry.org/docs/versions/'];
 export const supportsRanges = true;
 export const supportedRangeStrategies = ['bump', 'extend', 'pin', 'replace'];
 
-function notEmpty(s: string): boolean {
-  return s !== '';
+function equals(a: string, b: string): boolean {
+  return npm.equals(poetry2semver(a), poetry2semver(b));
 }
 
-function getVersionParts(input: string): [string, string] {
-  const versionParts = input.split('-');
-  if (versionParts.length === 1) {
-    return [input, ''];
-  }
-
-  return [versionParts[0], '-' + versionParts[1]];
+function getMajor(version: string): number {
+  return npm.getMajor(poetry2semver(version));
 }
 
-function padZeroes(input: string): string {
-  if (/[~^*]/.test(input)) {
-    // ignore ranges
-    return input;
-  }
-
-  const [output, stability] = getVersionParts(input);
-
-  const sections = output.split('.');
-  while (sections.length < 3) {
-    sections.push('0');
-  }
-  return sections.join('.') + stability;
-}
-
-// This function works like cargo2npm, but it doesn't
-// add a '^', because poetry treats versions without operators as
-// exact versions.
-function poetry2npm(input: string): string {
-  return input
-    .split(',')
-    .map((str) => str.trim())
-    .filter(notEmpty)
-    .join(' ');
-}
-
-// NOTE: This function is copied from cargo versioning code.
-// Poetry uses commas (like in cargo) instead of spaces (like in npm)
-// for AND operation.
-function npm2poetry(input: string): string {
-  // Note: this doesn't remove the ^
-  const res = input
-    .split(' ')
-    .map((str) => str.trim())
-    .filter(notEmpty);
-  const operators = ['^', '~', '=', '>', '<', '<=', '>='];
-  for (let i = 0; i < res.length - 1; i += 1) {
-    if (operators.includes(res[i])) {
-      const newValue = res[i] + ' ' + res[i + 1];
-      res.splice(i, 2, newValue);
-    }
-  }
-  return res.join(', ').replace(/\s*,?\s*\|\|\s*,?\s*/, ' || ');
+function getMinor(version: string): number {
+  return npm.getMinor(poetry2semver(version));
 }
 
-const equals = (a: string, b: string): boolean => {
-  try {
-    return npm.equals(padZeroes(a), padZeroes(b));
-  } catch (err) /* istanbul ignore next */ {
-    return pep440.equals(a, b);
-  }
-};
-
-const getMajor = (version: string): number => {
-  try {
-    return npm.getMajor(padZeroes(version));
-  } catch (err) /* istanbul ignore next */ {
-    return pep440.getMajor(version);
-  }
-};
-
-const getMinor = (version: string): number => {
-  try {
-    return npm.getMinor(padZeroes(version));
-  } catch (err) /* istanbul ignore next */ {
-    return pep440.getMinor(version);
-  }
-};
-
-const getPatch = (version: string): number => {
-  try {
-    return npm.getPatch(padZeroes(version));
-  } catch (err) /* istanbul ignore next */ {
-    return pep440.getPatch(version);
-  }
-};
+function getPatch(version: string): number {
+  return npm.getPatch(poetry2semver(version));
+}
 
-const isGreaterThan = (a: string, b: string): boolean => {
-  try {
-    return npm.isGreaterThan(padZeroes(a), padZeroes(b));
-  } catch (err) /* istanbul ignore next */ {
-    return pep440.isGreaterThan(a, b);
-  }
-};
+function isVersion(input: string): boolean {
+  return VERSION_PATTERN.test(input);
+}
 
-const isLessThanRange = (version: string, range: string): boolean =>
-  npm.isVersion(padZeroes(version)) &&
-  npm.isLessThanRange(padZeroes(version), poetry2npm(range));
+function isGreaterThan(a: string, b: string): boolean {
+  return npm.isGreaterThan(poetry2semver(a), poetry2semver(b));
+}
 
-export const isValid = (input: string): string | boolean =>
-  npm.isValid(poetry2npm(input));
+function isLessThanRange(version: string, range: string): boolean {
+  return (
+    isVersion(version) &&
+    npm.isLessThanRange(poetry2semver(version), poetry2npm(range))
+  );
+}
 
-const isStable = (version: string): boolean => npm.isStable(padZeroes(version));
+export function isValid(input: string): string | boolean {
+  return npm.isValid(poetry2npm(input));
+}
 
-const isVersion = (input: string): string | boolean =>
-  npm.isVersion(padZeroes(input));
+function isStable(version: string): boolean {
+  return npm.isStable(poetry2semver(version));
+}
 
-const matches = (version: string, range: string): boolean =>
-  npm.isVersion(padZeroes(version)) &&
-  npm.matches(padZeroes(version), poetry2npm(range));
+function matches(version: string, range: string): boolean {
+  return (
+    isVersion(version) && npm.matches(poetry2semver(version), poetry2npm(range))
+  );
+}
 
-const getSatisfyingVersion = (versions: string[], range: string): string =>
-  npm.getSatisfyingVersion(versions, poetry2npm(range));
+function getSatisfyingVersion(versions: string[], range: string): string {
+  return semver2poetry(
+    npm.getSatisfyingVersion(
+      versions.map((version) => poetry2semver(version)),
+      poetry2npm(range)
+    )
+  );
+}
 
-const minSatisfyingVersion = (versions: string[], range: string): string =>
-  npm.minSatisfyingVersion(versions, poetry2npm(range));
+function minSatisfyingVersion(versions: string[], range: string): string {
+  return semver2poetry(
+    npm.minSatisfyingVersion(
+      versions.map((version) => poetry2semver(version)),
+      poetry2npm(range)
+    )
+  );
+}
 
-const isSingleVersion = (constraint: string): string | boolean =>
-  (constraint.trim().startsWith('=') &&
-    isVersion(constraint.trim().substring(1).trim())) ||
-  isVersion(constraint.trim());
+function isSingleVersion(constraint: string): string | boolean {
+  return (
+    (constraint.trim().startsWith('=') &&
+      isVersion(constraint.trim().substring(1).trim())) ||
+    isVersion(constraint.trim())
+  );
+}
 
 function handleShort(
   operator: string,
@@ -163,9 +115,9 @@ function getNewValue({
   if (rangeStrategy === 'replace') {
     const npmCurrentValue = poetry2npm(currentValue);
     try {
-      const massagedNewVersion = padZeroes(newVersion);
+      const massagedNewVersion = poetry2semver(newVersion);
       if (
-        npm.isVersion(massagedNewVersion) &&
+        isVersion(massagedNewVersion) &&
         npm.matches(massagedNewVersion, npmCurrentValue)
       ) {
         return currentValue;
@@ -193,7 +145,12 @@ function getNewValue({
       }
     }
   }
-  if (!npm.isVersion(newVersion)) {
+
+  // Explicitly check whether this is a fully-qualified version
+  if (
+    (VERSION_PATTERN.exec(newVersion)?.groups?.release || '').split('.')
+      .length !== 3
+  ) {
     logger.debug(
       'Cannot massage python version to npm - returning currentValue'
     );
@@ -203,8 +160,8 @@ function getNewValue({
     const newSemver = npm.getNewValue({
       currentValue: poetry2npm(currentValue),
       rangeStrategy,
-      currentVersion,
-      newVersion,
+      currentVersion: poetry2semver(currentVersion),
+      newVersion: poetry2semver(newVersion),
     });
     const newPoetry = npm2poetry(newSemver);
     return newPoetry;
@@ -218,7 +175,7 @@ function getNewValue({
 }
 
 function sortVersions(a: string, b: string): number {
-  return npm.sortVersions(padZeroes(a), padZeroes(b));
+  return npm.sortVersions(poetry2semver(a), poetry2semver(b));
 }
 
 export const api: VersioningApi = {
diff --git a/lib/versioning/poetry/patterns.ts b/lib/versioning/poetry/patterns.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7b9df8306986d541a4655eb0a806aad11db0cfb7
--- /dev/null
+++ b/lib/versioning/poetry/patterns.ts
@@ -0,0 +1,47 @@
+import { regEx } from '../../util/regex';
+
+/**
+ * regex used by poetry.core.version.Version to parse union of SemVer
+ * (with a subset of pre/post/dev tags) and PEP440
+ * see: https://github.com/python-poetry/poetry-core/blob/01c0472d9cef3e1a4958364122dd10358a9bd719/poetry/core/version/version.py
+ */
+
+// prettier-ignore
+export const VERSION_PATTERN = regEx(
+    [
+      '^',
+      'v?',
+      '(?:',
+        '(?:(?<epoch>[0-9]+)!)?',           // epoch
+        '(?<release>[0-9]+(?:\\.[0-9]+){0,2})', // release segment
+        '(?<pre>',                          // pre-release
+          '[-_.]?',
+          '(?<pre_l>(a|b|c|rc|alpha|beta|pre|preview))',
+          '[-_.]?',
+          '(?<pre_n>[0-9]+)?',
+        ')?',
+        '(?<post>',                         // post release
+          '(?:-(?<post_n1>[0-9]+))',
+          '|',
+          '(?:',
+            '[-_.]?',
+            '(?<post_l>post|rev|r)',
+            '[-_.]?',
+            '(?<post_n2>[0-9]+)?',
+          ')',
+        ')?',
+        '(?<dev>',                          // dev release
+          '[-_.]?',
+          '(?<dev_l>dev)',
+          '[-_.]?',
+          '(?<dev_n>[0-9]+)?',
+        ')?',
+      ')',
+      '(?:\\+(?<local>[a-z0-9]+(?:[-_.][a-z0-9]+)*))?', // local version
+      '$'
+    ].join('')
+  );
+
+export const RANGE_COMPARATOR_PATTERN = regEx(
+  /(\s*(?:\^|~|[><!]?=|[><]|\|\|)\s*)/
+);
diff --git a/lib/versioning/poetry/readme.md b/lib/versioning/poetry/readme.md
index a9ebe10b5e736e6963b55c01ed6dcdb7b4a6a3ac..8d2bb1109732a9f8264530f84c1b985b4b5cf480 100644
--- a/lib/versioning/poetry/readme.md
+++ b/lib/versioning/poetry/readme.md
@@ -1,3 +1,6 @@
 Poetry versioning is a little like a mix of PEP440 and SemVer.
 
-Currently Renovate's implementation is based off npm versioning, but it is being migrated to be based off PEP440 to be more compatible with Poetry's behavior.
+Currently Renovate's implementation is based off npm versioning.
+This works by parsing versions using the same patterns and similar normalization rules as Poetry, passing them to the npm versioning implementation, and then reversing the normalizations.
+This allows Renovate to meaningfully compare the SemVer-style versions allowed in `pyproject.toml` to the PEP440 representations used on PyPI.
+These are equivalent for major.minor.patch releases, but different for pre-, post-, and dev releases.
diff --git a/lib/versioning/poetry/transform.ts b/lib/versioning/poetry/transform.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4b89535c713e38a88611d8bdea70f1fb1da516d2
--- /dev/null
+++ b/lib/versioning/poetry/transform.ts
@@ -0,0 +1,146 @@
+import { parse } from 'semver';
+import { RANGE_COMPARATOR_PATTERN, VERSION_PATTERN } from './patterns';
+
+function parseLetterTag(
+  letter?: string,
+  number?: string
+): { letter?: string; number?: string } | null {
+  if (letter !== undefined) {
+    // apply the same normalizations as poetry
+    const spellings = {
+      alpha: 'a',
+      beta: 'b',
+      c: 'rc',
+      pre: 'rc',
+      preview: 'rc',
+      r: 'post',
+      rev: 'post',
+    };
+    return {
+      letter: spellings[letter] || letter,
+      number: number === undefined ? '0' : number,
+    };
+  }
+  if (letter === undefined && number !== undefined) {
+    return { letter: 'post', number };
+  }
+  return null;
+}
+
+function notEmpty(s: string): boolean {
+  return s !== '';
+}
+
+/**
+ * Parse versions like poetry.core.masonry.version.Version does (union of SemVer
+ * and PEP440, with normalization of certain prerelease tags), and emit in SemVer
+ * format. NOTE: this silently discards the epoch field in PEP440 versions, as
+ * it has no equivalent in SemVer.
+ */
+export function poetry2semver(
+  poetry_version: string,
+  padRelease = true
+): string | null {
+  const match = VERSION_PATTERN.exec(poetry_version);
+  if (!match) {
+    return null;
+  }
+  // trim leading zeros from valid numbers
+  const releaseParts = match.groups.release
+    .split('.')
+    .map((segment) => parseInt(segment, 10));
+  while (padRelease && releaseParts.length < 3) {
+    releaseParts.push(0);
+  }
+  const pre = parseLetterTag(match.groups.pre_l, match.groups.pre_n);
+  const post = match.groups.post_n1
+    ? parseLetterTag(undefined, match.groups.post_n1)
+    : parseLetterTag(match.groups.post_l, match.groups.post_n);
+  const dev = parseLetterTag(match.groups.dev_l, match.groups.dev_n);
+
+  const parts = [releaseParts.map((num) => num.toString()).join('.')];
+  if (pre !== null) {
+    parts.push(`-${pre.letter}.${pre.number}`);
+  }
+  if (post !== null) {
+    parts.push(`-${post.letter}.${post.number}`);
+  }
+  if (dev !== null) {
+    parts.push(`-${dev.letter}.${dev.number}`);
+  }
+
+  return parts.join('');
+}
+
+/** Reverse normalizations applied by poetry2semver */
+export function semver2poetry(version?: string): string | null {
+  if (!version) {
+    return null;
+  }
+  const s = parse(version);
+  if (!s) {
+    return null;
+  }
+  const spellings = {
+    a: 'alpha',
+    b: 'beta',
+    c: 'rc',
+    dev: 'alpha',
+  };
+  s.prerelease = s.prerelease.map((letter) => spellings[letter] || letter);
+  return s.format();
+}
+
+/**
+ * Translate a poetry-style version range to npm format
+ *
+ * This function works like cargo2npm, but it doesn't
+ * add a '^', because poetry treats versions without operators as
+ * exact versions.
+ */
+export function poetry2npm(input: string): string {
+  // replace commas with spaces, then split at valid semver comparators
+  const chunks = input
+    .split(',')
+    .map((str) => str.trim())
+    .filter(notEmpty)
+    .join(' ')
+    .split(RANGE_COMPARATOR_PATTERN);
+  // do not pad versions with zeros in a range
+  const transformed = chunks
+    .map((chunk) => poetry2semver(chunk, false) || chunk)
+    .join('')
+    .replace(/===/, '=');
+  return transformed;
+}
+
+/**
+ * Translate an npm-style version range to poetry format
+ *
+ * NOTE: This function is largely copied from cargo versioning code.
+ * Poetry uses commas (like in cargo) instead of spaces (like in npm)
+ * for AND operation.
+ */
+export function npm2poetry(range: string): string {
+  // apply poetry-style normalizations to versions embedded in range string
+  // (i.e. anything that is not a range operator, potentially surrounded by whitespace)
+  const transformedRange = range
+    .split(RANGE_COMPARATOR_PATTERN)
+    .map((chunk) => semver2poetry(chunk) || chunk)
+    .join('');
+
+  // Note: this doesn't remove the ^
+  const res = transformedRange
+    .split(' ')
+    .map((str) => str.trim())
+    .filter(notEmpty);
+
+  const operators = ['^', '~', '=', '>', '<', '<=', '>='];
+  for (let i = 0; i < res.length - 1; i += 1) {
+    if (operators.includes(res[i])) {
+      const newValue = res[i] + ' ' + res[i + 1];
+      res.splice(i, 2, newValue);
+    }
+  }
+  return res.join(', ').replace(/\s*,?\s*\|\|\s*,?\s*/, ' || ');
+}