diff --git a/data/extract.py b/data/extract.py
index e49aa52fd8cdceef0ad871e5d1e1c276261f4929..a40ee5c125be6b4e0d6107ab24cb0831d1c5cf06 100644
--- a/data/extract.py
+++ b/data/extract.py
@@ -25,6 +25,7 @@ def invoke(mock1, mock2):
   # called arguments are in `mock_setup.call_args`
   call_args = mock1.call_args or mock2.call_args
   args, kwargs = call_args
-  print(json.dumps(kwargs, indent=2))
+  with open('renovate-pip_setup-report.json', 'w', encoding='utf-8') as f:
+    json.dump(kwargs, f, ensure_ascii=False, indent=2)
 
 invoke()
diff --git a/lib/manager/pip_setup/__snapshots__/extract.spec.ts.snap b/lib/manager/pip_setup/__snapshots__/extract.spec.ts.snap
index b4d6963c1797aef45a6bfab6efc02e748638d356..a61a5f4029a6d328774941f8fa81b1ba63ea0bd1 100644
--- a/lib/manager/pip_setup/__snapshots__/extract.spec.ts.snap
+++ b/lib/manager/pip_setup/__snapshots__/extract.spec.ts.snap
@@ -1,8 +1,8 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`lib/manager/pip_setup/extract getPythonAlias returns the python alias to use 1`] = `"python3.8"`;
+exports[`manager/pip_setup/extract getPythonAlias returns the python alias to use 1`] = `"python3.8"`;
 
-exports[`lib/manager/pip_setup/extract getPythonAlias returns the python alias to use 2`] = `
+exports[`manager/pip_setup/extract getPythonAlias returns the python alias to use 2`] = `
 Array [
   Object {
     "cmd": "python --version",
diff --git a/lib/manager/pip_setup/__snapshots__/index.spec.ts.snap b/lib/manager/pip_setup/__snapshots__/index.spec.ts.snap
index 77b2ecd780d81533d9425c0ae686e24ac4021c68..3e073102d1982534761849b5b8f4bece3a6b572e 100644
--- a/lib/manager/pip_setup/__snapshots__/index.spec.ts.snap
+++ b/lib/manager/pip_setup/__snapshots__/index.spec.ts.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`lib/manager/pip_setup/index extractPackageFile() catches error 1`] = `
+exports[`manager/pip_setup/index extractPackageFile() catches error 1`] = `
 Array [
   Object {
     "cmd": "python --version",
@@ -73,7 +73,7 @@ Array [
 ]
 `;
 
-exports[`lib/manager/pip_setup/index extractPackageFile() returns found deps (docker) 1`] = `
+exports[`manager/pip_setup/index extractPackageFile() returns found deps (docker) 1`] = `
 Object {
   "deps": Array [
     Object {
@@ -201,7 +201,7 @@ Object {
 }
 `;
 
-exports[`lib/manager/pip_setup/index extractPackageFile() returns found deps 1`] = `
+exports[`manager/pip_setup/index extractPackageFile() returns found deps 1`] = `
 Object {
   "deps": Array [
     Object {
@@ -329,7 +329,7 @@ Object {
 }
 `;
 
-exports[`lib/manager/pip_setup/index extractPackageFile() returns found deps 2`] = `
+exports[`manager/pip_setup/index extractPackageFile() returns found deps 2`] = `
 Array [
   Object {
     "cmd": "python --version",
@@ -402,7 +402,80 @@ Array [
 ]
 `;
 
-exports[`lib/manager/pip_setup/index extractPackageFile() should return null for invalid file 1`] = `
+exports[`manager/pip_setup/index extractPackageFile() returns no deps 1`] = `
+Array [
+  Object {
+    "cmd": "python --version",
+    "options": Object {
+      "cwd": null,
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 900000,
+    },
+  },
+  Object {
+    "cmd": "python3 --version",
+    "options": Object {
+      "cwd": null,
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 900000,
+    },
+  },
+  Object {
+    "cmd": "python3.8 --version",
+    "options": Object {
+      "cwd": null,
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 900000,
+    },
+  },
+  Object {
+    "cmd": "<extract.py> \\"lib/manager/pip_setup/__fixtures__/setup.py\\"",
+    "options": Object {
+      "cwd": "/tmp/github/some/repo",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 5000,
+    },
+  },
+]
+`;
+
+exports[`manager/pip_setup/index extractPackageFile() should return null for invalid file 1`] = `
 Array [
   Object {
     "cmd": "python --version",
diff --git a/lib/manager/pip_setup/extract.spec.ts b/lib/manager/pip_setup/extract.spec.ts
index 97ababb696ecb27c530756dfb25801938a7aeaf4..3f8e80f2236db4a64b1397d56b6eb529bfb2e8c6 100644
--- a/lib/manager/pip_setup/extract.spec.ts
+++ b/lib/manager/pip_setup/extract.spec.ts
@@ -1,8 +1,5 @@
-import { exec as _exec } from 'child_process';
-
-import { envMock, mockExecSequence } from '../../../test/execUtil';
-import { mocked } from '../../../test/util';
-import * as _env from '../../util/exec/env';
+import { envMock, exec, mockExecSequence } from '../../../test/execUtil';
+import { env, getName } from '../../../test/util';
 import {
   getPythonAlias,
   parsePythonVersion,
@@ -10,13 +7,10 @@ import {
   resetModule,
 } from './extract';
 
-const exec: jest.Mock<typeof _exec> = _exec as any;
-const env = mocked(_env);
-
 jest.mock('child_process');
 jest.mock('../../util/exec/env');
 
-describe('lib/manager/pip_setup/extract', () => {
+describe(getName(__filename), () => {
   beforeEach(() => {
     jest.resetAllMocks();
     jest.resetModules();
@@ -39,7 +33,9 @@ describe('lib/manager/pip_setup/extract', () => {
       const result = await getPythonAlias();
       expect(pythonVersions).toContain(result);
       expect(result).toMatchSnapshot();
+      expect(await getPythonAlias()).toEqual(result);
       expect(execSnapshots).toMatchSnapshot();
+      expect(execSnapshots).toHaveLength(3);
     });
   });
 });
diff --git a/lib/manager/pip_setup/extract.ts b/lib/manager/pip_setup/extract.ts
index 13baa017e9591a160d441dc40d43b686ec11d02d..f38d8efd5e070137df6fb7a8924f2c87120de33e 100644
--- a/lib/manager/pip_setup/extract.ts
+++ b/lib/manager/pip_setup/extract.ts
@@ -1,17 +1,16 @@
 import * as datasourcePypi from '../../datasource/pypi';
 import { logger } from '../../logger';
 import { SkipReason } from '../../types';
-import { resolveFile } from '../../util';
 import { exec } from '../../util/exec';
 import { BinarySource } from '../../util/exec/common';
 import { isSkipComment } from '../../util/ignore';
 import { ExtractConfig, PackageDependency, PackageFile } from '../common';
 import { dependencyPattern } from '../pip_requirements/extract';
+import { PythonSetup, copyExtractFile, parseReport } from './util';
 
 export const pythonVersions = ['python', 'python3', 'python3.8'];
 let pythonAlias: string | null = null;
 
-// istanbul ignore next
 export function resetModule(): void {
   pythonAlias = null;
 }
@@ -22,7 +21,6 @@ export function parsePythonVersion(str: string): number[] {
 }
 
 export async function getPythonAlias(): Promise<string> {
-  // istanbul ignore if
   if (pythonAlias) {
     return pythonAlias;
   }
@@ -34,18 +32,12 @@ export async function getPythonAlias(): Promise<string> {
       if (version[0] >= 3 && version[1] >= 7) {
         pythonAlias = pythonVersion;
       }
-    } catch (err) /* istanbul ignore next */ {
+    } catch (err) {
       logger.debug(`${pythonVersion} alias not found`);
     }
   }
   return pythonAlias;
 }
-interface PythonSetup {
-  extras_require: string[];
-  install_requires: string[];
-}
-
-let extractPy;
 
 export async function extractSetupFile(
   _content: string,
@@ -53,30 +45,10 @@ export async function extractSetupFile(
   config: ExtractConfig
 ): Promise<PythonSetup> {
   const cwd = config.localDir;
-  let cmd: string;
-  extractPy = extractPy || (await resolveFile('data/extract.py'));
+  let cmd = 'python';
+  const extractPy = await copyExtractFile();
   const args = [`"${extractPy}"`, `"${packageFile}"`];
-  if (config.binarySource === BinarySource.Docker) {
-    logger.debug('Running python via docker');
-    await exec(`docker pull renovate/pip`);
-    cmd = 'docker';
-    args.unshift(
-      'run',
-      '-i',
-      '--rm',
-      // volume
-      '-v',
-      `${cwd}:${cwd}`,
-      '-v',
-      `${extractPy}:${extractPy}`,
-      // cwd
-      '-w',
-      cwd,
-      // image
-      'renovate/pip',
-      'python'
-    );
-  } else {
+  if (config.binarySource !== BinarySource.Docker) {
     logger.debug('Running python via global command');
     cmd = await getPythonAlias();
   }
@@ -84,8 +56,10 @@ export async function extractSetupFile(
   const res = await exec(`${cmd} ${args.join(' ')}`, {
     cwd,
     timeout: 5000,
+    docker: {
+      image: 'renovate/pip',
+    },
   });
-  // istanbul ignore if
   if (res.stderr) {
     const stderr = res.stderr
       .replace(/.*\n\s*import imp/, '')
@@ -95,7 +69,7 @@ export async function extractSetupFile(
       logger.warn({ stdout: res.stdout, stderr }, 'Error in read setup file');
     }
   }
-  return JSON.parse(res.stdout);
+  return parseReport();
 }
 
 export async function extractPackageFile(
@@ -155,7 +129,6 @@ export async function extractPackageFile(
         ? a.depName.localeCompare(b.depName)
         : a.managerData.lineNumber - b.managerData.lineNumber
     );
-  // istanbul ignore if
   if (!deps.length) {
     return null;
   }
diff --git a/lib/manager/pip_setup/index.spec.ts b/lib/manager/pip_setup/index.spec.ts
index f3fdc84bc36786f3e15c607e3bab556cdc02c17c..c17f90d2f56a3de0384edaf79a8bbbc6996ea096 100644
--- a/lib/manager/pip_setup/index.spec.ts
+++ b/lib/manager/pip_setup/index.spec.ts
@@ -1,14 +1,14 @@
-import { exec as _exec } from 'child_process';
 import { readFileSync } from 'fs';
 import {
   ExecSnapshots,
   envMock,
+  exec,
   mockExecAll,
   mockExecSequence,
 } from '../../../test/execUtil';
-import { mocked } from '../../../test/util';
+import { env, getName } from '../../../test/util';
 import { BinarySource } from '../../util/exec/common';
-import * as _env from '../../util/exec/env';
+import * as fs from '../../util/fs';
 import * as extract from './extract';
 import { extractPackageFile } from '.';
 
@@ -22,9 +22,6 @@ const config = {
   localDir: '/tmp/github/some/repo',
 };
 
-const exec: jest.Mock<typeof _exec> = _exec as any;
-const env = mocked(_env);
-
 jest.mock('child_process');
 jest.mock('../../util/exec/env');
 
@@ -38,10 +35,10 @@ const pythonVersionCallResults = [
 const fixSnapshots = (snapshots: ExecSnapshots): ExecSnapshots =>
   snapshots.map((snapshot) => ({
     ...snapshot,
-    cmd: snapshot.cmd.replace(/^.*\/extract\.py"\s+/, '<extract.py> '),
+    cmd: snapshot.cmd.replace(/^.*extract\.py"\s+/, '<extract.py> '),
   }));
 
-describe('lib/manager/pip_setup/index', () => {
+describe(getName(__filename), () => {
   describe('extractPackageFile()', () => {
     beforeEach(() => {
       jest.resetAllMocks();
@@ -49,32 +46,57 @@ describe('lib/manager/pip_setup/index', () => {
       extract.resetModule();
 
       env.getChildProcessEnv.mockReturnValue(envMock.basic);
+
+      // do not copy extract.py
+      jest.spyOn(fs, 'writeLocalFile').mockResolvedValue();
     });
+
     it('returns found deps', async () => {
       const execSnapshots = mockExecSequence(exec, [
         ...pythonVersionCallResults,
-        { stdout: jsonContent, stderr: '' },
+        {
+          stdout: '',
+          stderr:
+            'DeprecationWarning: the imp module is deprecated in favour of importlib',
+        },
       ]);
+      jest.spyOn(fs, 'readLocalFile').mockResolvedValueOnce(jsonContent);
       expect(
         await extractPackageFile(content, packageFile, config)
       ).toMatchSnapshot();
       expect(exec).toHaveBeenCalledTimes(4);
       expect(fixSnapshots(execSnapshots)).toMatchSnapshot();
     });
+
     it('returns found deps (docker)', async () => {
       const execSnapshots = mockExecSequence(exec, [
-        { stdout: '', stderr: '' }, // docker pull
-        { stdout: jsonContent, stderr: '' },
+        { stdout: '', stderr: '' },
       ]);
 
+      jest.spyOn(fs, 'readLocalFile').mockResolvedValueOnce(jsonContent);
       expect(
         await extractPackageFile(content, packageFile, {
           ...config,
           binarySource: BinarySource.Docker,
         })
       ).toMatchSnapshot();
-      expect(execSnapshots).toHaveLength(2); // TODO: figure out volume arguments in Windows
+      expect(execSnapshots).toHaveLength(1); // TODO: figure out volume arguments in Windows
+    });
+
+    it('returns no deps', async () => {
+      const execSnapshots = mockExecSequence(exec, [
+        ...pythonVersionCallResults,
+        {
+          stdout: '',
+          stderr: 'fatal: No names found, cannot describe anything.',
+        },
+      ]);
+      jest.spyOn(fs, 'readLocalFile').mockResolvedValueOnce('{}');
+      expect(await extractPackageFile(content, packageFile, config)).toBeNull();
+      expect(exec).toHaveBeenCalledTimes(4);
+      expect(fixSnapshots(execSnapshots)).toMatchSnapshot();
     });
+
     it('should return null for invalid file', async () => {
       const execSnapshots = mockExecSequence(exec, [
         ...pythonVersionCallResults,
diff --git a/lib/manager/pip_setup/util.ts b/lib/manager/pip_setup/util.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7169f41cd950b4591f36a7ed1ffede8da290da4c
--- /dev/null
+++ b/lib/manager/pip_setup/util.ts
@@ -0,0 +1,29 @@
+import { resolveFile } from '../../util';
+import { readFile, readLocalFile, writeLocalFile } from '../../util/fs';
+
+// need to match filename in `data/extract.py`
+const REPORT = 'renovate-pip_setup-report.json';
+const EXTRACT = 'renovate-pip_setup-extract.py';
+
+let extractPy: string | undefined;
+
+export async function copyExtractFile(): Promise<string> {
+  if (extractPy === undefined) {
+    const file = await resolveFile('data/extract.py');
+    extractPy = await readFile(file, 'utf8');
+  }
+
+  await writeLocalFile(EXTRACT, extractPy);
+
+  return EXTRACT;
+}
+
+export interface PythonSetup {
+  extras_require: Record<string, string[]>;
+  install_requires: string[];
+}
+
+export async function parseReport(): Promise<PythonSetup> {
+  const data = await readLocalFile(REPORT, 'utf8');
+  return JSON.parse(data);
+}
diff --git a/test/execUtil.ts b/test/execUtil.ts
index 479f33559765b33855e645886f2ceb9438201a24..6e12081cc5811cb5b9fdde88774b01f05bb01506 100644
--- a/test/execUtil.ts
+++ b/test/execUtil.ts
@@ -8,6 +8,9 @@ type CallOptions = ExecOptions | null | undefined;
 
 export type ExecResult = { stdout: string; stderr: string } | Error;
 
+export type ExecMock = jest.Mock<typeof _exec>;
+export const exec: ExecMock = _exec as any;
+
 interface ExecSnapshot {
   cmd: string;
   options?: ExecOptions | null | undefined;
@@ -35,7 +38,7 @@ export function execSnapshot(cmd: string, options?: CallOptions): ExecSnapshot {
 const defaultExecResult = { stdout: '', stderr: '' };
 
 export function mockExecAll(
-  execFn: jest.Mock<typeof _exec>,
+  execFn: ExecMock,
   execResult: ExecResult = defaultExecResult
 ): ExecSnapshots {
   const snapshots: ExecSnapshots = [];
@@ -51,7 +54,7 @@ export function mockExecAll(
 }
 
 export function mockExecSequence(
-  execFn: jest.Mock<typeof _exec>,
+  execFn: ExecMock,
   execResults: ExecResult[]
 ): ExecSnapshots {
   const snapshots: ExecSnapshots = [];