diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index 8855775f9d5ed11c707530dd3f62f8eebad970b2..ef109b2932ab45a3917b6657faa18fb9764d3339 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -1226,6 +1226,19 @@ const options = [
     mergeable: true,
     cli: false,
   },
+  {
+    name: 'pipenv',
+    releaseStatus: 'beta',
+    description: 'Configuration object for Pipfile files',
+    stage: 'package',
+    type: 'json',
+    default: {
+      enabled: false,
+      fileMatch: ['(^|/)Pipfile$'],
+    },
+    mergeable: true,
+    cli: false,
+  },
   {
     name: 'python',
     description: 'Configuration object for python',
diff --git a/lib/manager/index.js b/lib/manager/index.js
index f9379a1e8abffdb3043eb07ec14bf6f33b492fbe..e4a67138b3596040586f009b693fbd55b1b6a04f 100644
--- a/lib/manager/index.js
+++ b/lib/manager/index.js
@@ -15,6 +15,7 @@ const managerList = [
   'nvm',
   'pip_requirements',
   'pip_setup',
+  'pipenv',
   'terraform',
   'travis',
   'nuget',
diff --git a/lib/manager/pipenv/artifacts.js b/lib/manager/pipenv/artifacts.js
new file mode 100644
index 0000000000000000000000000000000000000000..7118e2db1fe0961d1d3f0c2b6de6ffc807241a03
--- /dev/null
+++ b/lib/manager/pipenv/artifacts.js
@@ -0,0 +1,104 @@
+const { exec } = require('child-process-promise');
+const fs = require('fs-extra');
+const os = require('os');
+const upath = require('upath');
+
+module.exports = {
+  getArtifacts,
+};
+
+async function getArtifacts(
+  pipfileName,
+  updatedDeps,
+  newPipfileContent,
+  config
+) {
+  logger.debug(`pipenv.getArtifacts(${pipfileName})`);
+  process.env.PIPENV_CACHE_DIR =
+    process.env.PIPENV_CACHE_DIR ||
+    upath.join(os.tmpdir(), '/renovate/cache/pipenv');
+  await fs.ensureDir(process.env.PIPENV_CACHE_DIR);
+  logger.debug('Using pipenv cache ' + process.env.PIPENV_CACHE_DIR);
+  const lockFileName = pipfileName + '.lock';
+  const existingLockFileContent = await platform.getFile(lockFileName);
+  if (!existingLockFileContent) {
+    logger.debug('No Pipfile.lock found');
+    return null;
+  }
+  const cwd = upath.join(config.localDir, upath.dirname(pipfileName));
+  let stdout;
+  let stderr;
+  try {
+    const localPipfileFileName = upath.join(config.localDir, pipfileName);
+    await fs.outputFile(localPipfileFileName, newPipfileContent);
+    const localLockFileName = upath.join(config.localDir, lockFileName);
+    const env =
+      config.global && config.global.exposeEnv
+        ? process.env
+        : {
+            HOME: process.env.HOME,
+            PATH: process.env.PATH,
+            LC_ALL: process.env.LC_ALL,
+            LANG: process.env.LANG,
+            PIPENV_CACHE_DIR: process.env.PIPENV_CACHE_DIR,
+          };
+    const startTime = process.hrtime();
+    let cmd;
+    if (config.binarySource === 'docker') {
+      logger.info('Running pipenv via docker');
+      cmd = `docker run --rm `;
+      const volumes = [config.localDir];
+      cmd += volumes.map(v => `-v ${v}:${v} `).join('');
+      const envVars = ['LC_ALL', 'LANG', 'PIPENV_CACHE_DIR'];
+      cmd += envVars.map(e => `-e ${e} `).join('');
+      cmd += `-w ${cwd} `;
+      cmd += `renovate/pipenv pipenv`;
+    } else {
+      logger.info('Running pipenv via global command');
+      cmd = 'pipenv';
+    }
+    const args = 'lock';
+    logger.debug({ cmd, args }, 'pipenv lock command');
+    ({ stdout, stderr } = await exec(`${cmd} ${args}`, {
+      cwd,
+      shell: true,
+      env,
+    }));
+    const duration = process.hrtime(startTime);
+    const seconds = Math.round(duration[0] + duration[1] / 1e9);
+    stdout = stdout ? stdout.replace(/(Locking|Running)[^\s]*?\s/g, '') : null;
+    logger.info(
+      { seconds, type: 'Pipfile.lock', stdout, stderr },
+      'Generated lockfile'
+    );
+    // istanbul ignore if
+    if (config.gitFs) {
+      const status = await platform.getRepoStatus();
+      if (!status.modified.includes(lockFileName)) {
+        return null;
+      }
+    } else {
+      const newLockFileContent = await fs.readFile(localLockFileName, 'utf8');
+
+      if (newLockFileContent === existingLockFileContent) {
+        logger.debug('Pipfile.lock is unchanged');
+        return null;
+      }
+    }
+    logger.debug('Returning updated Pipfile.lock');
+    return {
+      file: {
+        name: lockFileName,
+        contents: await fs.readFile(localLockFileName, 'utf8'),
+      },
+    };
+  } catch (err) {
+    logger.warn({ err, message: err.message }, 'Failed to update Pipfile.lock');
+    return {
+      lockFileError: {
+        lockFile: lockFileName,
+        stderr: err.message,
+      },
+    };
+  }
+}
diff --git a/lib/manager/pipenv/extract.js b/lib/manager/pipenv/extract.js
new file mode 100644
index 0000000000000000000000000000000000000000..8457e66da97ec067619ee0ac40c879d93c6c1abd
--- /dev/null
+++ b/lib/manager/pipenv/extract.js
@@ -0,0 +1,89 @@
+const toml = require('toml');
+
+// based on https://www.python.org/dev/peps/pep-0508/#names
+const packageRegex = /^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$/i;
+const rangePattern = require('@renovate/pep440/lib/specifier').RANGE_PATTERN;
+
+const specifierPartPattern = `\\s*${rangePattern.replace(
+  /\?<\w+>/g,
+  '?:'
+)}\\s*`;
+const specifierPattern = `${specifierPartPattern}(?:,${specifierPartPattern})*`;
+
+module.exports = {
+  extractPackageFile,
+};
+
+function extractPackageFile(content) {
+  logger.debug('pipenv.extractPackageFile()');
+  let pipfile;
+  try {
+    pipfile = toml.parse(content);
+  } catch (err) {
+    logger.debug({ err }, 'Error parsing Pipfile');
+    return null;
+  }
+  let registryUrls;
+  if (pipfile.source) {
+    registryUrls = pipfile.source.map(source =>
+      source.url.replace(/simple(\/)?$/, 'pypi/')
+    );
+  }
+
+  const deps = [
+    ...extractFromSection(pipfile, 'packages', registryUrls),
+    ...extractFromSection(pipfile, 'dev-packages', registryUrls),
+  ];
+  if (!deps.length) {
+    return null;
+  }
+  return { deps };
+}
+
+function extractFromSection(pipfile, section, registryUrls) {
+  if (!(section in pipfile)) {
+    return [];
+  }
+  const specifierRegex = new RegExp(`^${specifierPattern}$`);
+  const deps = Object.entries(pipfile[section])
+    .map(x => {
+      const [depName, requirements] = x;
+      let currentValue;
+      let pipenvNestedVersion;
+      if (requirements.version) {
+        currentValue = requirements.version;
+        pipenvNestedVersion = true;
+      } else {
+        currentValue = requirements;
+        pipenvNestedVersion = false;
+      }
+      const packageMatches = packageRegex.exec(depName);
+      const specifierMatches = specifierRegex.exec(currentValue);
+      if (!packageMatches) {
+        logger.debug(
+          `Skipping dependency with malformed package name "${depName}".`
+        );
+        return null;
+      }
+      if (!specifierMatches) {
+        logger.debug(
+          `Skipping dependency with malformed version specifier "${currentValue}".`
+        );
+        return null;
+      }
+      const dep = {
+        depName,
+        currentValue,
+        pipenvNestedVersion,
+        purl: 'pkg:pypi/' + depName,
+        versionScheme: 'pep440',
+        depType: section,
+      };
+      if (registryUrls) {
+        dep.registryUrls = registryUrls;
+      }
+      return dep;
+    })
+    .filter(Boolean);
+  return deps;
+}
diff --git a/lib/manager/pipenv/index.js b/lib/manager/pipenv/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..e2001f3d8c1b3a3dd89c46ba3fa25a7c64cc1cd2
--- /dev/null
+++ b/lib/manager/pipenv/index.js
@@ -0,0 +1,12 @@
+const { extractPackageFile } = require('./extract');
+const { updateDependency } = require('./update');
+const { getArtifacts } = require('./artifacts');
+
+const language = 'python';
+
+module.exports = {
+  extractPackageFile,
+  updateDependency,
+  getArtifacts,
+  language,
+};
diff --git a/lib/manager/pipenv/update.js b/lib/manager/pipenv/update.js
new file mode 100644
index 0000000000000000000000000000000000000000..995d71e33357551bdb826761a1bad7a145f3de1f
--- /dev/null
+++ b/lib/manager/pipenv/update.js
@@ -0,0 +1,80 @@
+const _ = require('lodash');
+const toml = require('toml');
+
+module.exports = {
+  updateDependency,
+};
+
+// Return true if the match string is found at index in content
+function matchAt(content, index, match) {
+  return content.substring(index, index + match.length) === match;
+}
+
+// Replace oldString with newString at location index of content
+function replaceAt(content, index, oldString, newString) {
+  logger.debug(`Replacing ${oldString} with ${newString} at index ${index}`);
+  return (
+    content.substr(0, index) +
+    newString +
+    content.substr(index + oldString.length)
+  );
+}
+
+function updateDependency(fileContent, upgrade) {
+  try {
+    const { depType, depName, newValue, pipenvNestedVersion } = upgrade;
+    logger.debug(`pipenv.updateDependency(): ${newValue}`);
+    const parsedContents = toml.parse(fileContent);
+    let oldVersion;
+    if (pipenvNestedVersion) {
+      oldVersion = parsedContents[depType][depName].version;
+    } else {
+      oldVersion = parsedContents[depType][depName];
+    }
+    if (oldVersion === newValue) {
+      logger.info('Version is already updated');
+      return fileContent;
+    }
+    if (pipenvNestedVersion) {
+      parsedContents[depType][depName].version = newValue;
+    } else {
+      parsedContents[depType][depName] = newValue;
+    }
+    const searchString = `"${oldVersion}"`;
+    const newString = `"${newValue}"`;
+    let newFileContent = null;
+    let searchIndex = fileContent.indexOf(`[${depType}]`) + depType.length;
+    for (; searchIndex < fileContent.length; searchIndex += 1) {
+      // First check if we have a hit for the old version
+      if (matchAt(fileContent, searchIndex, searchString)) {
+        logger.trace(`Found match at index ${searchIndex}`);
+        // Now test if the result matches
+        const testContent = replaceAt(
+          fileContent,
+          searchIndex,
+          searchString,
+          newString
+        );
+        // Compare the parsed toml structure of old and new
+        if (_.isEqual(parsedContents, toml.parse(testContent))) {
+          newFileContent = testContent;
+          break;
+        } else {
+          logger.debug('Mismatched replace at searchIndex ' + searchIndex);
+        }
+      }
+    }
+    // istanbul ignore if
+    if (!newFileContent) {
+      logger.info(
+        { fileContent, parsedContents, depType, depName, newValue },
+        'Warning: updateDependency error'
+      );
+      return fileContent;
+    }
+    return newFileContent;
+  } catch (err) {
+    logger.info({ err }, 'Error setting new package version');
+    return null;
+  }
+}
diff --git a/package.json b/package.json
index b1381616b96194c7077d8473f50465b5519a1700..e0ce39d2eccfd5d85abfe7e97b4174ab257bcde2 100644
--- a/package.json
+++ b/package.json
@@ -115,6 +115,8 @@
     "semver-utils": "1.1.4",
     "simple-git": "1.107.0",
     "slugify": "1.3.3",
+    "toml": "2.3.3",
+    "tomlify-j0.4": "3.0.0",
     "traverse": "0.6.6",
     "upath": "1.1.0",
     "validator": "10.9.0",
diff --git a/test/_fixtures/pipenv/Pipfile1 b/test/_fixtures/pipenv/Pipfile1
new file mode 100644
index 0000000000000000000000000000000000000000..2780a7a308442a9d0e4b230af4fc6bb9d1f8029e
--- /dev/null
+++ b/test/_fixtures/pipenv/Pipfile1
@@ -0,0 +1,22 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[[source]]
+url = "http://example.com/private-pypi/"
+verify_ssl = false
+name = "private-pypi"
+
+[packages]
+some-package = "==0.3.1"
+some-other-package = "==1.0.0"
+"_invalid-package" = "==1.0.0"
+invalid-version = "==0 0"
+pytest-benchmark = {version = "==1.0.0", extras = ["histogram"]}
+
+[dev-packages]
+dev-package = "==0.1.0"
+
+[requires]
+python_version = "3.6"
diff --git a/test/_fixtures/pipenv/Pipfile2 b/test/_fixtures/pipenv/Pipfile2
new file mode 100644
index 0000000000000000000000000000000000000000..b84f8069426ec0b5a59c54d00c242097f3ef162f
--- /dev/null
+++ b/test/_fixtures/pipenv/Pipfile2
@@ -0,0 +1,17 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+Django = "==1"
+distribute = "==0.6.27"
+dj-database-url = "==0.2"
+psycopg2 = "==2.4.5"
+wsgiref = "==0.1.2"
+
+[dev-packages]
+
+[requires]
+python_version = "3.6"
+
diff --git a/test/manager/__snapshots__/manager-docs.spec.js.snap b/test/manager/__snapshots__/manager-docs.spec.js.snap
index 837f06458ff1e26faa9b73b0d46bb464d9901e56..c51cf469ec0462a730cded1cfa168913c3174c47 100644
--- a/test/manager/__snapshots__/manager-docs.spec.js.snap
+++ b/test/manager/__snapshots__/manager-docs.spec.js.snap
@@ -20,6 +20,7 @@ Array [
   "nvm",
   "pip_requirements",
   "pip_setup",
+  "pipenv",
   "terraform",
   "travis",
 ]
diff --git a/test/manager/pipenv/__snapshots__/artifacts.spec.js.snap b/test/manager/pipenv/__snapshots__/artifacts.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..7c50564902f60324e3d5ec6b34838cb45fa572bc
--- /dev/null
+++ b/test/manager/pipenv/__snapshots__/artifacts.spec.js.snap
@@ -0,0 +1,10 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`.getArtifacts() catches errors 1`] = `
+Object {
+  "lockFileError": Object {
+    "lockFile": "Pipfile.lock",
+    "stderr": "not found",
+  },
+}
+`;
diff --git a/test/manager/pipenv/__snapshots__/extract.spec.js.snap b/test/manager/pipenv/__snapshots__/extract.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..571a1b7e4aa014e6283bad6a7ee6aa18b2fb412d
--- /dev/null
+++ b/test/manager/pipenv/__snapshots__/extract.spec.js.snap
@@ -0,0 +1,114 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`lib/manager/pipenv/extract extractPackageFile() extracts dependencies 1`] = `
+Array [
+  Object {
+    "currentValue": "==0.3.1",
+    "depName": "some-package",
+    "depType": "packages",
+    "pipenvNestedVersion": false,
+    "purl": "pkg:pypi/some-package",
+    "registryUrls": Array [
+      "https://pypi.org/pypi/",
+      "http://example.com/private-pypi/",
+    ],
+    "versionScheme": "pep440",
+  },
+  Object {
+    "currentValue": "==1.0.0",
+    "depName": "some-other-package",
+    "depType": "packages",
+    "pipenvNestedVersion": false,
+    "purl": "pkg:pypi/some-other-package",
+    "registryUrls": Array [
+      "https://pypi.org/pypi/",
+      "http://example.com/private-pypi/",
+    ],
+    "versionScheme": "pep440",
+  },
+  Object {
+    "currentValue": "==1.0.0",
+    "depName": "pytest-benchmark",
+    "depType": "packages",
+    "pipenvNestedVersion": true,
+    "purl": "pkg:pypi/pytest-benchmark",
+    "registryUrls": Array [
+      "https://pypi.org/pypi/",
+      "http://example.com/private-pypi/",
+    ],
+    "versionScheme": "pep440",
+  },
+  Object {
+    "currentValue": "==0.1.0",
+    "depName": "dev-package",
+    "depType": "dev-packages",
+    "pipenvNestedVersion": false,
+    "purl": "pkg:pypi/dev-package",
+    "registryUrls": Array [
+      "https://pypi.org/pypi/",
+      "http://example.com/private-pypi/",
+    ],
+    "versionScheme": "pep440",
+  },
+]
+`;
+
+exports[`lib/manager/pipenv/extract extractPackageFile() extracts multiple dependencies 1`] = `
+Array [
+  Object {
+    "currentValue": "==1",
+    "depName": "Django",
+    "depType": "packages",
+    "pipenvNestedVersion": false,
+    "purl": "pkg:pypi/Django",
+    "registryUrls": Array [
+      "https://pypi.org/pypi/",
+    ],
+    "versionScheme": "pep440",
+  },
+  Object {
+    "currentValue": "==0.6.27",
+    "depName": "distribute",
+    "depType": "packages",
+    "pipenvNestedVersion": false,
+    "purl": "pkg:pypi/distribute",
+    "registryUrls": Array [
+      "https://pypi.org/pypi/",
+    ],
+    "versionScheme": "pep440",
+  },
+  Object {
+    "currentValue": "==0.2",
+    "depName": "dj-database-url",
+    "depType": "packages",
+    "pipenvNestedVersion": false,
+    "purl": "pkg:pypi/dj-database-url",
+    "registryUrls": Array [
+      "https://pypi.org/pypi/",
+    ],
+    "versionScheme": "pep440",
+  },
+  Object {
+    "currentValue": "==2.4.5",
+    "depName": "psycopg2",
+    "depType": "packages",
+    "pipenvNestedVersion": false,
+    "purl": "pkg:pypi/psycopg2",
+    "registryUrls": Array [
+      "https://pypi.org/pypi/",
+    ],
+    "versionScheme": "pep440",
+  },
+  Object {
+    "currentValue": "==0.1.2",
+    "depName": "wsgiref",
+    "depType": "packages",
+    "pipenvNestedVersion": false,
+    "purl": "pkg:pypi/wsgiref",
+    "registryUrls": Array [
+      "https://pypi.org/pypi/",
+    ],
+    "versionScheme": "pep440",
+  },
+]
+`;
diff --git a/test/manager/pipenv/__snapshots__/update.spec.js.snap b/test/manager/pipenv/__snapshots__/update.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..ec1f8a30684317d050b149855bb9fe3e7d9a6f52
--- /dev/null
+++ b/test/manager/pipenv/__snapshots__/update.spec.js.snap
@@ -0,0 +1,79 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`manager/pipenv/update updateDependency replaces existing value 1`] = `
+"[[source]]
+url = \\"https://pypi.org/simple\\"
+verify_ssl = true
+name = \\"pypi\\"
+
+[[source]]
+url = \\"http://example.com/private-pypi/\\"
+verify_ssl = false
+name = \\"private-pypi\\"
+
+[packages]
+some-package = \\"==1.0.1\\"
+some-other-package = \\"==1.0.0\\"
+\\"_invalid-package\\" = \\"==1.0.0\\"
+invalid-version = \\"==0 0\\"
+pytest-benchmark = {version = \\"==1.0.0\\", extras = [\\"histogram\\"]}
+
+[dev-packages]
+dev-package = \\"==0.1.0\\"
+
+[requires]
+python_version = \\"3.6\\"
+"
+`;
+
+exports[`manager/pipenv/update updateDependency replaces nested value 1`] = `
+"[[source]]
+url = \\"https://pypi.org/simple\\"
+verify_ssl = true
+name = \\"pypi\\"
+
+[[source]]
+url = \\"http://example.com/private-pypi/\\"
+verify_ssl = false
+name = \\"private-pypi\\"
+
+[packages]
+some-package = \\"==0.3.1\\"
+some-other-package = \\"==1.0.0\\"
+\\"_invalid-package\\" = \\"==1.0.0\\"
+invalid-version = \\"==0 0\\"
+pytest-benchmark = {version = \\"==1.9.1\\", extras = [\\"histogram\\"]}
+
+[dev-packages]
+dev-package = \\"==0.1.0\\"
+
+[requires]
+python_version = \\"3.6\\"
+"
+`;
+
+exports[`manager/pipenv/update updateDependency upgrades dev packages 1`] = `
+"[[source]]
+url = \\"https://pypi.org/simple\\"
+verify_ssl = true
+name = \\"pypi\\"
+
+[[source]]
+url = \\"http://example.com/private-pypi/\\"
+verify_ssl = false
+name = \\"private-pypi\\"
+
+[packages]
+some-package = \\"==0.3.1\\"
+some-other-package = \\"==1.0.0\\"
+\\"_invalid-package\\" = \\"==1.0.0\\"
+invalid-version = \\"==0 0\\"
+pytest-benchmark = {version = \\"==1.0.0\\", extras = [\\"histogram\\"]}
+
+[dev-packages]
+dev-package = \\"==0.2.0\\"
+
+[requires]
+python_version = \\"3.6\\"
+"
+`;
diff --git a/test/manager/pipenv/artifacts.spec.js b/test/manager/pipenv/artifacts.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..809f3986a975dff74c0bbb8f0213dc0e7d24202c
--- /dev/null
+++ b/test/manager/pipenv/artifacts.spec.js
@@ -0,0 +1,63 @@
+jest.mock('fs-extra');
+jest.mock('child-process-promise');
+jest.mock('../../../lib/util/host-rules');
+
+const fs = require('fs-extra');
+const { exec } = require('child-process-promise');
+const pipenv = require('../../../lib/manager/pipenv/artifacts');
+
+const config = {
+  localDir: '/tmp/github/some/repo',
+};
+
+describe('.getArtifacts()', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+  it('returns if no Pipfile.lock found', async () => {
+    expect(await pipenv.getArtifacts('Pipfile', [], '', config)).toBeNull();
+  });
+  it('returns null if unchanged', async () => {
+    platform.getFile.mockReturnValueOnce('Current Pipfile.lock');
+    exec.mockReturnValueOnce({
+      stdout: '',
+      stderror: '',
+    });
+    fs.readFile = jest.fn(() => 'Current Pipfile.lock');
+    expect(await pipenv.getArtifacts('Pipfile', [], '{}', config)).toBeNull();
+  });
+  it('returns updated Pipfile.lock', async () => {
+    platform.getFile.mockReturnValueOnce('Current Pipfile.lock');
+    exec.mockReturnValueOnce({
+      stdout: '',
+      stderror: '',
+    });
+    fs.readFile = jest.fn(() => 'New Pipfile.lock');
+    expect(
+      await pipenv.getArtifacts('Pipfile', [], '{}', config)
+    ).not.toBeNull();
+  });
+  it('supports docker mode', async () => {
+    platform.getFile.mockReturnValueOnce('Current Pipfile.lock');
+    exec.mockReturnValueOnce({
+      stdout: '',
+      stderror: '',
+    });
+    fs.readFile = jest.fn(() => 'New Pipfile.lock');
+    expect(
+      await pipenv.getArtifacts('Pipfile', [], '{}', {
+        ...config,
+        binarySource: 'docker',
+      })
+    ).not.toBeNull();
+  });
+  it('catches errors', async () => {
+    platform.getFile.mockReturnValueOnce('Current Pipfile.lock');
+    fs.outputFile = jest.fn(() => {
+      throw new Error('not found');
+    });
+    expect(
+      await pipenv.getArtifacts('Pipfile', [], '{}', config)
+    ).toMatchSnapshot();
+  });
+});
diff --git a/test/manager/pipenv/extract.spec.js b/test/manager/pipenv/extract.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b4d9180ce4197db27ac3cdee4a56d7eaa3f58f62
--- /dev/null
+++ b/test/manager/pipenv/extract.spec.js
@@ -0,0 +1,59 @@
+const fs = require('fs');
+const { extractPackageFile } = require('../../../lib/manager/pipenv/extract');
+
+const pipfile1 = fs.readFileSync('test/_fixtures/pipenv/Pipfile1', 'utf8');
+const pipfile2 = fs.readFileSync('test/_fixtures/pipenv/Pipfile2', 'utf8');
+
+describe('lib/manager/pipenv/extract', () => {
+  describe('extractPackageFile()', () => {
+    let config;
+    beforeEach(() => {
+      config = {};
+    });
+    it('returns null for empty', () => {
+      expect(extractPackageFile('[packages]\r\n', config)).toBe(null);
+    });
+    it('returns null for invalid toml file', () => {
+      expect(extractPackageFile('nothing here', config)).toBe(null);
+    });
+    it('extracts dependencies', () => {
+      const res = extractPackageFile(pipfile1, config).deps;
+      expect(res).toMatchSnapshot();
+      expect(res).toHaveLength(4);
+    });
+    it('extracts multiple dependencies', () => {
+      const res = extractPackageFile(pipfile2, config).deps;
+      expect(res).toMatchSnapshot();
+      expect(res).toHaveLength(5);
+    });
+    it('ignores invalid package names', () => {
+      const content = '[packages]\r\nfoo = "==1.0.0"\r\n_invalid = "==1.0.0"';
+      const res = extractPackageFile(content, config).deps;
+      expect(res).toHaveLength(1);
+    });
+    it('ignores invalid versions', () => {
+      const content = '[packages]\r\nfoo = "==1.0.0"\r\nsome-package = "==0 0"';
+      const res = extractPackageFile(content, config).deps;
+      expect(res).toHaveLength(1);
+    });
+    it('extracts all sources', () => {
+      const content =
+        '[[source]]\r\nurl = "source-url"\r\n' +
+        '[[source]]\r\nurl = "other-source-url"\r\n' +
+        '[packages]\r\nfoo = "==1.0.0"\r\n';
+      const res = extractPackageFile(content, config).deps;
+      expect(res[0].registryUrls).toEqual(['source-url', 'other-source-url']);
+    });
+    it('converts simple-API URLs to JSON-API URLs', () => {
+      const content =
+        '[[source]]\r\nurl = "https://my-pypi/foo/simple/"\r\n' +
+        '[[source]]\r\nurl = "https://other-pypi/foo/simple"\r\n' +
+        '[packages]\r\nfoo = "==1.0.0"\r\n';
+      const res = extractPackageFile(content, config).deps;
+      expect(res[0].registryUrls).toEqual([
+        'https://my-pypi/foo/pypi/',
+        'https://other-pypi/foo/pypi/',
+      ]);
+    });
+  });
+});
diff --git a/test/manager/pipenv/update.spec.js b/test/manager/pipenv/update.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..cff86fa89b51aa2e8b02162aca3020e9352a3ea3
--- /dev/null
+++ b/test/manager/pipenv/update.spec.js
@@ -0,0 +1,56 @@
+const fs = require('fs');
+const { updateDependency } = require('../../../lib/manager/pipenv/update');
+
+const pipfile = fs.readFileSync('test/_fixtures/pipenv/Pipfile1', 'utf8');
+
+describe('manager/pipenv/update', () => {
+  describe('updateDependency', () => {
+    it('replaces existing value', () => {
+      const upgrade = {
+        depName: 'some-package',
+        newValue: '==1.0.1',
+        depType: 'packages',
+      };
+      const res = updateDependency(pipfile, upgrade);
+      expect(res).not.toEqual(pipfile);
+      expect(res.includes(upgrade.newValue)).toBe(true);
+      expect(res).toMatchSnapshot();
+    });
+    it('handles already replace values', () => {
+      const upgrade = {
+        depName: 'some-package',
+        newValue: '==0.3.1',
+        depType: 'packages',
+      };
+      const res = updateDependency(pipfile, upgrade);
+      expect(res).toEqual(pipfile);
+    });
+    it('replaces nested value', () => {
+      const upgrade = {
+        depName: 'pytest-benchmark',
+        newValue: '==1.9.1',
+        depType: 'packages',
+        pipenvNestedVersion: true,
+      };
+      const res = updateDependency(pipfile, upgrade);
+      expect(res).not.toEqual(pipfile);
+      expect(res.includes(upgrade.newValue)).toBe(true);
+      expect(res).toMatchSnapshot();
+    });
+    it('upgrades dev packages', () => {
+      const upgrade = {
+        depName: 'dev-package',
+        newValue: '==0.2.0',
+        depType: 'dev-packages',
+      };
+      const res = updateDependency(pipfile, upgrade);
+      expect(res).not.toEqual(pipfile);
+      expect(res.includes(upgrade.newValue)).toBe(true);
+      expect(res).toMatchSnapshot();
+    });
+    it('returns null if error', () => {
+      const res = updateDependency(null, null);
+      expect(res).toBe(null);
+    });
+  });
+});
diff --git a/test/workers/repository/extract/__snapshots__/index.spec.js.snap b/test/workers/repository/extract/__snapshots__/index.spec.js.snap
index b45f5f37dbb0ecc88708ab7b8b9e51228b9f3f23..1bd977c380156a4bf18eb53d9a2a91509cd18b42 100644
--- a/test/workers/repository/extract/__snapshots__/index.spec.js.snap
+++ b/test/workers/repository/extract/__snapshots__/index.spec.js.snap
@@ -56,6 +56,9 @@ Object {
   "pip_setup": Array [
     Object {},
   ],
+  "pipenv": Array [
+    Object {},
+  ],
   "terraform": Array [
     Object {},
   ],
diff --git a/website/docs/configuration-options.md b/website/docs/configuration-options.md
index 710b02ce15b41b4668dc5f83e790f13319e4fd36..f0fe54f71b3814d340db303a621e84e189c6036f 100644
--- a/website/docs/configuration-options.md
+++ b/website/docs/configuration-options.md
@@ -578,6 +578,12 @@ Add configuration here to specifically override settings for `setup.py` files.
 
 Warning: `setup.py` support is currently in beta, so is not enabled by default. You will need to configure `{ "pip_setup": { "enabled": true }}" to enable.
 
+## pipenv
+
+Add configuration here to change pipenv settings, e.g. to change the file pattern for pipenv so that you can use filenames other than Pipfile.
+
+Warning: 'pipenv' support is currently in beta, so it is not enabled by default. You will need to configure `{ "pipenv": { "enabled": true }}" to enable.
+
 ## prBodyColumns
 
 Use this array to provide a list of column names you wish to include in the PR tables.
diff --git a/website/docs/python.md b/website/docs/python.md
index 753f3ad7bc395651459a899b88799fd810c3a769..650a9a88e5453a25898bf17ce83cfd991f176613 100644
--- a/website/docs/python.md
+++ b/website/docs/python.md
@@ -5,7 +5,11 @@ description: Python/pip dependencies support in Renovate
 
 # Python Package Manager Support
 
-Renovate supports upgrading dependencies in `pip` requirements (e.g. `requirements.txt`, `requirements.pip`) files.
+Renovate supports the following Python package managers:
+
+- `pip` (e.g. `requirements.txt`, `requirements.pip`) files
+- `pipenv` (e.g. `Pipfile`)
+- `setup.py`
 
 ## Versioning Support
 
@@ -13,14 +17,28 @@ The [PEP440](https://www.python.org/dev/peps/pep-0440/) versioning scheme has be
 
 ## How It Works
 
-1.  Renovate will search each repository for any requirements files it finds.
+1.  Renovate will search each repository for any package files it finds.
 2.  Existing dependencies will be extracted from the file(s)
 3.  Renovate will look up the latest version on [PyPI](https://pypi.org/) to determine if any upgrades are available
 4.  If the source package includes a GitHub URL as its source, and has either a "changelog" file or uses GitHub releases, then Release Notes for each version will be embedded in the generated PR.
 
+## Enabling Beta
+
+Both `pipenv` and `setup.py` are classified a "beta", so they are not enabled by default. To enable them, you need to add configuration like the following to your `renovate.json` file:
+
+```json
+{
+  "pipenv": {
+    "enabled": false
+  }
+}
+```
+
+Note: if you _only_ have these package files and no other package files (like `package.json`, `Dockerfile`, etc) then Renovate won't detect them and you won't get an onboarding PR. In that case you need to add Renovate configuration manually to skip the onboarding step.
+
 ## Alternative file names
 
-The default file matching regex for requirements.txt aims to pick up the most popular conventions for file naming, but it's possible that some get missed. If you have a specific file or file pattern you want to get found by Renovate, then you can do this by adding a new pattern under the `fileMatch` field of `pip_requirements`. e.g. you could add this to your config:
+The default file matching regex for `requirements.txt` aims to pick up the most popular conventions for file naming, but it's possible that some get missed. If you have a specific file or file pattern you want to get found by Renovate, then you can do this by adding a new pattern under the `fileMatch` field of `pip_requirements`. e.g. you could add this to your config:
 
 ```json
   "pip_requirements": {
@@ -42,6 +60,10 @@ some-package==0.3.1
 some-other-package==1.0.0
 ```
 
+#### Sources in `Pipfile`
+
+Renovate will detect any custom-configured sources in `Pipfile` and use them.
+
 #### Specify URL in configuration
 
 The configuration option `registryUrls` can be used to configure an alternate index URL. Example:
@@ -72,4 +94,4 @@ Alternatively, maybe you only want one package manager, such as `npm`. In that c
 
 ## Future work
 
-Feature requests are open for conda support, additional file types (e.g. `setup.cfg`), and of course `pipenv` support. You can locate these issues by filtering on the [#python](https://github.com/renovatebot/renovate/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%23python) hashtag in the repository. Please +1 and/or add a comment to each issue that would benefit you so that we can gauge the popularity/importance of each.
+Feature requests are open for conda support and additional file types (e.g. `setup.cfg`). You can locate these issues by filtering on the [#python](https://github.com/renovatebot/renovate/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%23python) hashtag in the repository. Please +1 and/or add a comment to each issue that would benefit you so that we can gauge the popularity/importance of each.
diff --git a/yarn.lock b/yarn.lock
index c6a211d0556b108b4cdc052a94ae9ea7d04fb229..c04287935863e9434b60355bd6f91a4154d84bf7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8219,6 +8219,16 @@ to-vfile@^2.2.0:
     vfile "^2.0.0"
     x-is-function "^1.0.4"
 
+toml@2.3.3:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/toml/-/toml-2.3.3.tgz#8d683d729577cb286231dfc7a8affe58d31728fb"
+  integrity sha512-O7L5hhSQHxuufWUdcTRPfuTh3phKfAZ/dqfxZFoxPCj2RYmpaSGLEIs016FCXItQwNr08yefUB5TSjzRYnajTA==
+
+tomlify-j0.4@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/tomlify-j0.4/-/tomlify-j0.4-3.0.0.tgz#99414d45268c3a3b8bf38be82145b7bba34b7473"
+  integrity sha512-2Ulkc8T7mXJ2l0W476YC/A209PR38Nw8PuaCNtk9uI3t1zzFdGQeWYGQvmj2PZkVvRC/Yoi4xQKMRnWc/N29tQ==
+
 tough-cookie@2.0.x:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.0.0.tgz#41ce08720b35cf90beb044dd2609fb19e928718f"