diff --git a/lib/manager/api.ts b/lib/manager/api.ts
index 295cb5dfae9973025259f5367b136b0696549423..f6e05be9c587294f51c234f3295f1f7ee8bd656a 100644
--- a/lib/manager/api.ts
+++ b/lib/manager/api.ts
@@ -43,6 +43,7 @@ import * as nodenv from './nodenv';
 import * as npm from './npm';
 import * as nuget from './nuget';
 import * as nvm from './nvm';
+import * as pipCompile from './pip-compile';
 import * as pip_requirements from './pip_requirements';
 import * as pip_setup from './pip_setup';
 import * as pipenv from './pipenv';
@@ -110,6 +111,7 @@ api.set('nodenv', nodenv);
 api.set('npm', npm);
 api.set('nuget', nuget);
 api.set('nvm', nvm);
+api.set('pip-compile', pipCompile);
 api.set('pip_requirements', pip_requirements);
 api.set('pip_setup', pip_setup);
 api.set('pipenv', pipenv);
diff --git a/lib/manager/pip-compile/__snapshots__/artifacts.spec.ts.snap b/lib/manager/pip-compile/__snapshots__/artifacts.spec.ts.snap
new file mode 100644
index 0000000000000000000000000000000000000000..7fa37078f87784a9da2add329656891eaec4f85c
--- /dev/null
+++ b/lib/manager/pip-compile/__snapshots__/artifacts.spec.ts.snap
@@ -0,0 +1,151 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`.updateArtifacts() catches errors 1`] = `
+Array [
+  Object {
+    "artifactError": Object {
+      "lockFile": "requirements.txt",
+      "stderr": "not found",
+    },
+  },
+]
+`;
+
+exports[`.updateArtifacts() returns null if unchanged 1`] = `
+Array [
+  Object {
+    "cmd": "pip-compile",
+    "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",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
+exports[`.updateArtifacts() returns updated requirements.txt 1`] = `
+Array [
+  Object {
+    "cmd": "pip-compile",
+    "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",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
+exports[`.updateArtifacts() returns updated requirements.txt when doing lockfile maintenance 1`] = `
+Array [
+  Object {
+    "cmd": "pip-compile",
+    "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",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
+exports[`.updateArtifacts() supports docker mode 1`] = `
+Array [
+  Object {
+    "cmd": "docker pull renovate/python",
+    "options": Object {
+      "encoding": "utf-8",
+    },
+  },
+  Object {
+    "cmd": "docker ps --filter name=renovate_python -aq",
+    "options": Object {
+      "encoding": "utf-8",
+    },
+  },
+  Object {
+    "cmd": "docker run --rm --name=renovate_python --label=renovate_child -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -v \\"/tmp/renovate/cache\\":\\"/tmp/renovate/cache\\" -w \\"/tmp/github/some/repo\\" renovate/python bash -l -c \\"pip install --user pip-tools && pip-compile\\"",
+    "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",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
+exports[`.updateArtifacts() uses pipenv version from config 1`] = `
+Array [
+  Object {
+    "cmd": "docker pull renovate/python",
+    "options": Object {
+      "encoding": "utf-8",
+    },
+  },
+  Object {
+    "cmd": "docker ps --filter name=renovate_python -aq",
+    "options": Object {
+      "encoding": "utf-8",
+    },
+  },
+  Object {
+    "cmd": "docker run --rm --name=renovate_python --label=renovate_child -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -v \\"/tmp/renovate/cache\\":\\"/tmp/renovate/cache\\" -w \\"/tmp/github/some/repo\\" renovate/python bash -l -c \\"pip install --user pip-tools1.2.3 && pip-compile\\"",
+    "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",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
diff --git a/lib/manager/pip-compile/artifacts.spec.ts b/lib/manager/pip-compile/artifacts.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cf8f52af8b750813f2d553115b2a9d26ebdd575e
--- /dev/null
+++ b/lib/manager/pip-compile/artifacts.spec.ts
@@ -0,0 +1,159 @@
+import { exec as _exec } from 'child_process';
+import _fs from 'fs-extra';
+import { join } from 'upath';
+import { envMock, mockExecAll } from '../../../test/exec-util';
+import { git, mocked } from '../../../test/util';
+import { setAdminConfig } from '../../config/admin';
+import type { RepoAdminConfig } from '../../config/types';
+import * as docker from '../../util/exec/docker';
+import * as _env from '../../util/exec/env';
+import type { StatusResult } from '../../util/git';
+import type { UpdateArtifactsConfig } from '../types';
+import * as pipCompile from './artifacts';
+
+jest.mock('fs-extra');
+jest.mock('child_process');
+jest.mock('../../util/exec/env');
+jest.mock('../../util/git');
+jest.mock('../../util/host-rules');
+jest.mock('../../util/http');
+
+const fs: jest.Mocked<typeof _fs> = _fs as any;
+const exec: jest.Mock<typeof _exec> = _exec as any;
+const env = mocked(_env);
+
+const adminConfig: RepoAdminConfig = {
+  // `join` fixes Windows CI
+  localDir: join('/tmp/github/some/repo'),
+  cacheDir: join('/tmp/renovate/cache'),
+};
+const dockerAdminConfig = { ...adminConfig, binarySource: 'docker' };
+
+const config: UpdateArtifactsConfig = {};
+const lockMaintenanceConfig = { ...config, isLockFileMaintenance: true };
+
+describe('.updateArtifacts()', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+    env.getChildProcessEnv.mockReturnValue({
+      ...envMock.basic,
+      LANG: 'en_US.UTF-8',
+      LC_ALL: 'en_US',
+    });
+    setAdminConfig(adminConfig);
+    docker.resetPrefetchedImages();
+  });
+
+  it('returns if no requirements.txt found', async () => {
+    expect(
+      await pipCompile.updateArtifacts({
+        packageFileName: 'requirements.in',
+        updatedDeps: [],
+        newPackageFileContent: '',
+        config,
+      })
+    ).toBeNull();
+  });
+
+  it('returns null if unchanged', async () => {
+    fs.readFile.mockResolvedValueOnce('content' as any);
+    const execSnapshots = mockExecAll(exec);
+    fs.readFile.mockReturnValueOnce('content' as any);
+    expect(
+      await pipCompile.updateArtifacts({
+        packageFileName: 'requirements.in',
+        updatedDeps: [],
+        newPackageFileContent: 'some new content',
+        config,
+      })
+    ).toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+
+  it('returns updated requirements.txt', async () => {
+    fs.readFile.mockResolvedValueOnce('current requirements.txt' as any);
+    const execSnapshots = mockExecAll(exec);
+    git.getRepoStatus.mockResolvedValue({
+      modified: ['requirements.txt'],
+    } as StatusResult);
+    fs.readFile.mockReturnValueOnce('New requirements.txt' as any);
+    expect(
+      await pipCompile.updateArtifacts({
+        packageFileName: 'requirements.in',
+        updatedDeps: [],
+        newPackageFileContent: 'some new content',
+        config: { ...config, constraints: { python: '3.7' } },
+      })
+    ).not.toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+
+  it('supports docker mode', async () => {
+    setAdminConfig(dockerAdminConfig);
+    const execSnapshots = mockExecAll(exec);
+    git.getRepoStatus.mockResolvedValue({
+      modified: ['requirements.txt'],
+    } as StatusResult);
+    fs.readFile.mockReturnValueOnce('new lock' as any);
+    expect(
+      await pipCompile.updateArtifacts({
+        packageFileName: 'requirements.in',
+        updatedDeps: [],
+        newPackageFileContent: 'some new content',
+        config,
+      })
+    ).not.toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+
+  it('catches errors', async () => {
+    fs.readFile.mockResolvedValueOnce('Current requirements.txt' as any);
+    fs.outputFile.mockImplementationOnce(() => {
+      throw new Error('not found');
+    });
+    expect(
+      await pipCompile.updateArtifacts({
+        packageFileName: 'requirements.in',
+        updatedDeps: [],
+        newPackageFileContent: '{}',
+        config,
+      })
+    ).toMatchSnapshot();
+  });
+
+  it('returns updated requirements.txt when doing lockfile maintenance', async () => {
+    fs.readFile.mockResolvedValueOnce('Current requirements.txt' as any);
+    const execSnapshots = mockExecAll(exec);
+    git.getRepoStatus.mockResolvedValue({
+      modified: ['requirements.txt'],
+    } as StatusResult);
+    fs.readFile.mockReturnValueOnce('New requirements.txt' as any);
+    expect(
+      await pipCompile.updateArtifacts({
+        packageFileName: 'requirements.in',
+        updatedDeps: [],
+        newPackageFileContent: '{}',
+        config: lockMaintenanceConfig,
+      })
+    ).not.toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+
+  it('uses pipenv version from config', async () => {
+    setAdminConfig(dockerAdminConfig);
+    const execSnapshots = mockExecAll(exec);
+    git.getRepoStatus.mockResolvedValue({
+      modified: ['requirements.txt'],
+    } as StatusResult);
+    fs.readFile.mockReturnValueOnce('new lock' as any);
+    expect(
+      await pipCompile.updateArtifacts({
+        packageFileName: 'requirements.in',
+        updatedDeps: [],
+        newPackageFileContent: 'some new content',
+        config: { ...config, constraints: { pipTools: '1.2.3' } },
+      })
+    ).not.toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+});
diff --git a/lib/manager/pip-compile/artifacts.ts b/lib/manager/pip-compile/artifacts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..58056c07d93dc836012582004093613b668553ae
--- /dev/null
+++ b/lib/manager/pip-compile/artifacts.ts
@@ -0,0 +1,103 @@
+import is from '@sindresorhus/is';
+import { quote as pipCompile } from 'shlex';
+import { TEMPORARY_ERROR } from '../../constants/error-messages';
+import { logger } from '../../logger';
+import { ExecOptions, exec } from '../../util/exec';
+import { deleteLocalFile, readLocalFile, writeLocalFile } from '../../util/fs';
+import { getRepoStatus } from '../../util/git';
+import type {
+  UpdateArtifact,
+  UpdateArtifactsConfig,
+  UpdateArtifactsResult,
+} from '../types';
+
+function getPythonConstraint(
+  config: UpdateArtifactsConfig
+): string | undefined | null {
+  const { constraints = {} } = config;
+  const { python } = constraints;
+
+  if (python) {
+    logger.debug('Using python constraint from config');
+    return python;
+  }
+
+  return undefined;
+}
+
+function getPipToolsConstraint(config: UpdateArtifactsConfig): string {
+  const { constraints = {} } = config;
+  const { pipTools } = constraints;
+
+  if (is.string(pipTools)) {
+    logger.debug('Using pipTools constraint from config');
+    return pipTools;
+  }
+
+  return '';
+}
+
+export async function updateArtifacts({
+  packageFileName: inputFileName,
+  newPackageFileContent: newInputContent,
+  config,
+}: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
+  const outputFileName = inputFileName.replace(/(\.in)?$/, '.txt');
+  logger.debug(
+    `pipCompile.updateArtifacts(${inputFileName}->${outputFileName})`
+  );
+  const existingOutput = await readLocalFile(outputFileName, 'utf8');
+  if (!existingOutput) {
+    logger.debug('No pip-compile output file found');
+    return null;
+  }
+  try {
+    await writeLocalFile(inputFileName, newInputContent);
+    if (config.isLockFileMaintenance) {
+      await deleteLocalFile(outputFileName);
+    }
+    const cmd = 'pip-compile';
+    const tagConstraint = getPythonConstraint(config);
+    const pipToolsConstraint = getPipToolsConstraint(config);
+    const execOptions: ExecOptions = {
+      cwdFile: inputFileName,
+      docker: {
+        image: 'python',
+        tagConstraint,
+        tagScheme: 'pep440',
+        preCommands: [
+          `pip install --user ${pipCompile(`pip-tools${pipToolsConstraint}`)}`,
+        ],
+      },
+    };
+    logger.debug({ cmd }, 'pip-compile command');
+    await exec(cmd, execOptions);
+    const status = await getRepoStatus();
+    if (!status?.modified.includes(outputFileName)) {
+      return null;
+    }
+    logger.debug('Returning updated pip-compile result');
+    return [
+      {
+        file: {
+          name: outputFileName,
+          contents: await readLocalFile(outputFileName, 'utf8'),
+        },
+      },
+    ];
+  } catch (err) {
+    // istanbul ignore if
+    if (err.message === TEMPORARY_ERROR) {
+      throw err;
+    }
+    logger.debug({ err }, 'Failed to pip-compile');
+    return [
+      {
+        artifactError: {
+          lockFile: outputFileName,
+          stderr: err.message,
+        },
+      },
+    ];
+  }
+}
diff --git a/lib/manager/pip-compile/index.ts b/lib/manager/pip-compile/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..606569dcf2a0348eda5dea90b406fee7f6878461
--- /dev/null
+++ b/lib/manager/pip-compile/index.ts
@@ -0,0 +1,10 @@
+import { LANGUAGE_PYTHON } from '../../constants/languages';
+
+export { extractPackageFile } from '../pip_requirements/extract';
+export { updateArtifacts } from './artifacts';
+
+export const language = LANGUAGE_PYTHON;
+
+export const defaultConfig = {
+  fileMatch: [],
+};
diff --git a/lib/manager/pip-compile/readme.md b/lib/manager/pip-compile/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..42330b600641fb3c2b94f831cc11e1f54d521500
--- /dev/null
+++ b/lib/manager/pip-compile/readme.md
@@ -0,0 +1,42 @@
+Due to limited functionality, the `pip-compile` manager should be considered in an "alpha" stage, which means it's not ready for production use for the majority of end users.
+We welcome feedback and bug reports!
+
+The current implementation has some limitations.
+Read the full document before you start using the `pip-compile` manager.
+
+### Non-configured fileMatch
+
+The `pip-compile` manager has an empty array for default `fileMatch`, meaning it won't match any files ever by default.
+You can "activate" the manager by specifying a `fileMatch` pattern such as:
+
+```json
+{
+  "pip-compile": {
+    "fileMatch": ["(^|/)requirements\\.in$"]
+  }
+}
+```
+
+### Assumption of `.in`/`.txt`
+
+If Renovate matches/extracts a file, it assumes that the corresponding output file is found by swapping the `.in` for `.txt`.
+e.g. `requirements.in` => `requirements.txt`
+
+Therefore it will not work if files are in separate directories, including `input/requirements.in` and `output/requirements.txt`.
+
+If no `.in` suffix is found, then a `.txt` suffix is appended for the output file, e.g. `foo.file` would look for a corresponding `foo.file.txt`.
+
+We intend to make the mapping configurable in future iterations.
+
+### Configuration of Python version
+
+By default Renovate uses the latest version of Python.
+To get Renovate to use another version of Python, add a contraints` rule to the Renovate config:
+
+```json
+{
+  "constraints": {
+    "python": "3.7"
+  }
+}
+```