diff --git a/stix2/__init__.py b/stix2/__init__.py
index 904af9c..90bcf59 100644
--- a/stix2/__init__.py
+++ b/stix2/__init__.py
@@ -4,6 +4,7 @@
 
 from . import exceptions
 from .bundle import Bundle
+from .environment import ObjectFactory
 from .observables import (URL, AlternateDataStream, ArchiveExt, Artifact,
                           AutonomousSystem, Directory, DomainName,
                           EmailAddress, EmailMessage, EmailMIMEComponent, File,
diff --git a/stix2/environment.py b/stix2/environment.py
new file mode 100644
index 0000000..f855755
--- /dev/null
+++ b/stix2/environment.py
@@ -0,0 +1,68 @@
+import copy
+
+
+class ObjectFactory(object):
+    """Object Factory
+
+    Used to easily create STIX objects with default values for certain
+    properties.
+
+    Args:
+        created_by_ref: Default created_by_ref value to apply to all
+            objects created by this factory.
+        created: Default created value to apply to all
+            objects created by this factory.
+        external_references: Default `external_references` value to apply
+            to all objects created by this factory.
+        object_marking_refs: Default `object_marking_refs` value to apply
+            to all objects created by this factory.
+        list_append: When a default is set for a list property like
+            `external_references` or `object_marking_refs` and a value for
+            that property is passed into `create()`, if this is set to True,
+            that value will be added to the list alongside the default. If
+            this is set to False, the passed in value will replace the
+            default. Defaults to True.
+    """
+
+    def __init__(self, created_by_ref=None, created=None,
+                 external_references=None, object_marking_refs=None,
+                 list_append=True):
+
+        self._defaults = {}
+        if created_by_ref:
+            self._defaults['created_by_ref'] = created_by_ref
+        if created:
+            self._defaults['created'] = created
+            # If the user provides a default "created" time, we also want to use
+            # that as the modified time.
+            self._defaults['modified'] = created
+        if external_references:
+            self._defaults['external_references'] = external_references
+        if object_marking_refs:
+            self._defaults['object_marking_refs'] = object_marking_refs
+        self._list_append = list_append
+        self._list_properties = ['external_references', 'object_marking_refs']
+
+    def create(self, cls, **kwargs):
+        # Use self.defaults as the base, but update with any explicit args
+        # provided by the user.
+        properties = copy.deepcopy(self._defaults)
+        if kwargs:
+            if self._list_append:
+                # Append provided items to list properties instead of replacing them
+                for list_prop in set(self._list_properties).intersection(kwargs.keys(), properties.keys()):
+                    kwarg_prop = kwargs.pop(list_prop)
+                    if kwarg_prop is None:
+                        del properties[list_prop]
+                        continue
+                    if not isinstance(properties[list_prop], list):
+                        properties[list_prop] = [properties[list_prop]]
+
+                    if isinstance(kwarg_prop, list):
+                        properties[list_prop].extend(kwarg_prop)
+                    else:
+                        properties[list_prop].append(kwarg_prop)
+
+            properties.update(**kwargs)
+
+        return cls(**properties)
diff --git a/stix2/other.py b/stix2/other.py
index 51663b3..cd75745 100644
--- a/stix2/other.py
+++ b/stix2/other.py
@@ -69,7 +69,7 @@ class MarkingProperty(Property):
 class MarkingDefinition(_STIXBase):
     _type = 'marking-definition'
     _properties = {
-        'created': TimestampProperty(default=lambda: NOW, required=True),
+        'created': TimestampProperty(default=lambda: NOW),
         'external_references': ListProperty(ExternalReference),
         'created_by_ref': ReferenceProperty(type="identity"),
         'object_marking_refs': ListProperty(ReferenceProperty(type="marking-definition")),
diff --git a/stix2/properties.py b/stix2/properties.py
index 80e5345..71e4bd9 100644
--- a/stix2/properties.py
+++ b/stix2/properties.py
@@ -5,7 +5,7 @@ import inspect
 import re
 import uuid
 
-from six import text_type
+from six import string_types, text_type
 
 from .base import _Observable, _STIXBase
 from .exceptions import DictionaryKeyError
@@ -101,12 +101,9 @@ class ListProperty(Property):
             iter(value)
         except TypeError:
             raise ValueError("must be an iterable.")
-        try:
-            if isinstance(value, basestring):
-                value = [value]
-        except NameError:
-            if isinstance(value, str):
-                value = [value]
+
+        if isinstance(value, (_STIXBase, string_types)):
+            value = [value]
 
         result = []
         for item in value:

Create an Object Factory class
This would let users specify defaults for some common properties and easily create STIX objects without needing to specify them every single time; the objects would automatically be created with the default value.

For example, you could specify a default `created_by_ref` value to point to the Identity SDO for your organization.
diff --git a/stix2/test/constants.py b/stix2/test/constants.py
index b631d08..958120b 100644
--- a/stix2/test/constants.py
+++ b/stix2/test/constants.py
@@ -20,6 +20,12 @@ TOOL_ID = "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f"
 SIGHTING_ID = "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb"
 VULNERABILITY_ID = "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061"
 
+# Minimum required args for an Identity instance
+IDENTITY_KWARGS = dict(
+    name="John Smith",
+    identity_class="individual",
+)
+
 # Minimum required args for an Indicator instance
 INDICATOR_KWARGS = dict(
     labels=['malicious-activity'],
diff --git a/stix2/test/test_environment.py b/stix2/test/test_environment.py
new file mode 100644
index 0000000..9be8101
--- /dev/null
+++ b/stix2/test/test_environment.py
@@ -0,0 +1,83 @@
+import stix2
+
+from .constants import (FAKE_TIME, IDENTITY_ID, IDENTITY_KWARGS,
+                        INDICATOR_KWARGS)
+
+
+def test_object_factory_created_by_ref_str():
+    factory = stix2.ObjectFactory(created_by_ref=IDENTITY_ID)
+    ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS)
+    assert ind.created_by_ref == IDENTITY_ID
+
+
+def test_object_factory_created_by_ref_obj():
+    id_obj = stix2.Identity(id=IDENTITY_ID, **IDENTITY_KWARGS)
+    factory = stix2.ObjectFactory(created_by_ref=id_obj)
+    ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS)
+    assert ind.created_by_ref == IDENTITY_ID
+
+
+def test_object_factory_override_default():
+    factory = stix2.ObjectFactory(created_by_ref=IDENTITY_ID)
+    new_id = "identity--983b3172-44fe-4a80-8091-eb8098841fe8"
+    ind = factory.create(stix2.Indicator, created_by_ref=new_id, **INDICATOR_KWARGS)
+    assert ind.created_by_ref == new_id
+
+
+def test_object_factory_created():
+    factory = stix2.ObjectFactory(created=FAKE_TIME)
+    ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS)
+    assert ind.created == FAKE_TIME
+    assert ind.modified == FAKE_TIME
+
+
+def test_object_factory_external_resource():
+    ext_ref = stix2.ExternalReference(source_name="ACME Threat Intel",
+                                      description="Threat report")
+    factory = stix2.ObjectFactory(external_references=ext_ref)
+    ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS)
+    assert ind.external_references[0].source_name == "ACME Threat Intel"
+    assert ind.external_references[0].description == "Threat report"
+
+    ind2 = factory.create(stix2.Indicator, external_references=None, **INDICATOR_KWARGS)
+    assert 'external_references' not in ind2
+
+
+def test_object_factory_obj_markings():
+    stmt_marking = stix2.StatementMarking("Copyright 2016, Example Corp")
+    mark_def = stix2.MarkingDefinition(definition_type="statement",
+                                       definition=stmt_marking)
+    factory = stix2.ObjectFactory(object_marking_refs=[mark_def, stix2.TLP_AMBER])
+    ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS)
+    assert mark_def.id in ind.object_marking_refs
+    assert stix2.TLP_AMBER.id in ind.object_marking_refs
+
+    factory = stix2.ObjectFactory(object_marking_refs=stix2.TLP_RED)
+    ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS)
+    assert stix2.TLP_RED.id in ind.object_marking_refs
+
+
+def test_object_factory_list_append():
+    ext_ref = stix2.ExternalReference(source_name="ACME Threat Intel",
+                                      description="Threat report from ACME")
+    ext_ref2 = stix2.ExternalReference(source_name="Yet Another Threat Report",
+                                       description="Threat report from YATR")
+    ext_ref3 = stix2.ExternalReference(source_name="Threat Report #3",
+                                       description="One more threat report")
+    factory = stix2.ObjectFactory(external_references=ext_ref)
+    ind = factory.create(stix2.Indicator, external_references=ext_ref2, **INDICATOR_KWARGS)
+    assert ind.external_references[1].source_name == "Yet Another Threat Report"
+
+    ind = factory.create(stix2.Indicator, external_references=[ext_ref2, ext_ref3], **INDICATOR_KWARGS)
+    assert ind.external_references[2].source_name == "Threat Report #3"
+
+
+def test_object_factory_list_replace():
+    ext_ref = stix2.ExternalReference(source_name="ACME Threat Intel",
+                                      description="Threat report from ACME")
+    ext_ref2 = stix2.ExternalReference(source_name="Yet Another Threat Report",
+                                       description="Threat report from YATR")
+    factory = stix2.ObjectFactory(external_references=ext_ref, list_append=False)
+    ind = factory.create(stix2.Indicator, external_references=ext_ref2, **INDICATOR_KWARGS)
+    assert len(ind.external_references) == 1
+    assert ind.external_references[0].source_name == "Yet Another Threat Report"
diff --git a/stix2/test/test_markings.py b/stix2/test/test_markings.py
index ebfa480..70d67dd 100644
--- a/stix2/test/test_markings.py
+++ b/stix2/test/test_markings.py
@@ -29,6 +29,19 @@ EXPECTED_STATEMENT_MARKING_DEFINITION = """{
     "type": "marking-definition"
 }"""
 
+EXPECTED_CAMPAIGN_WITH_OBJECT_MARKING = """{
+    "created": "2016-04-06T20:03:00.000Z",
+    "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
+    "description": "Campaign by Green Group against a series of targets in the financial services sector.",
+    "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
+    "modified": "2016-04-06T20:03:00.000Z",
+    "name": "Green Group Attacks Against Finance",
+    "object_marking_refs": [
+        "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9"
+    ],
+    "type": "campaign"
+}"""
+
 EXPECTED_GRANULAR_MARKING = """{
     "marking_ref": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
     "selectors": [
@@ -84,6 +97,29 @@ def test_marking_def_example_with_positional_statement():
     assert str(marking_definition) == EXPECTED_STATEMENT_MARKING_DEFINITION
 
 
+def test_marking_def_invalid_type():
+    with pytest.raises(ValueError):
+        stix2.MarkingDefinition(
+            id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
+            created="2017-01-20T00:00:00.000Z",
+            definition_type="my-definiition-type",
+            definition=stix2.StatementMarking("Copyright 2016, Example Corp")
+        )
+
+
+def test_campaign_with_markings_example():
+    campaign = stix2.Campaign(
+        id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
+        created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
+        created="2016-04-06T20:03:00Z",
+        modified="2016-04-06T20:03:00Z",
+        name="Green Group Attacks Against Finance",
+        description="Campaign by Green Group against a series of targets in the financial services sector.",
+        object_marking_refs=TLP_WHITE
+    )
+    assert str(campaign) == EXPECTED_CAMPAIGN_WITH_OBJECT_MARKING
+
+
 def test_granular_example():
     granular_marking = stix2.GranularMarking(
         marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
@@ -119,7 +155,6 @@ def test_campaign_with_granular_markings_example():
                 marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
                 selectors=["description"])
             ])
-    print(str(campaign))
     assert str(campaign) == EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS
 
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest-cov coverage pytest
apt-get update apt-get install -y gcc
3.6
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
attrs==22.2.0
Babel==2.11.0
bump2version==1.0.1
bumpversion==0.6.0
certifi==2021.5.30
cfgv==3.3.1
charset-normalizer==2.0.12
coverage==6.2
distlib==0.3.9
docutils==0.18.1
filelock==3.4.1
identify==2.4.4
idna==3.10
imagesize==1.4.1
importlib-metadata==4.8.3
importlib-resources==5.2.3
iniconfig==1.1.1
Jinja2==3.0.3
MarkupSafe==2.0.1
nodeenv==1.6.0
packaging==21.3
platformdirs==2.4.0
pluggy==1.0.0
pre-commit==2.17.0
py==1.11.0
Pygments==2.14.0
pyparsing==3.1.4
pytest==7.0.1
pytest-cov==4.0.0
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.1
requests==2.27.1
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinx-prompt==1.5.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
-e git+https://github.com/oasis-open/cti-python-stix2.git@6f680be8a65028c303bae38bbe1fa0a2d08852a8#egg=stix2
toml==0.10.2
tomli==1.2.3
tox==3.28.0
typing_extensions==4.1.1
urllib3==1.26.20
virtualenv==20.16.2
zipp==3.6.0

name: cti-python-stix2
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - attrs==22.2.0
      - babel==2.11.0
      - bump2version==1.0.1
      - bumpversion==0.6.0
      - cfgv==3.3.1
      - charset-normalizer==2.0.12
      - coverage==6.2
      - distlib==0.3.9
      - docutils==0.18.1
      - filelock==3.4.1
      - identify==2.4.4
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.8.3
      - importlib-resources==5.2.3
      - iniconfig==1.1.1
      - jinja2==3.0.3
      - markupsafe==2.0.1
      - nodeenv==1.6.0
      - packaging==21.3
      - platformdirs==2.4.0
      - pluggy==1.0.0
      - pre-commit==2.17.0
      - py==1.11.0
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytest-cov==4.0.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==6.0.1
      - requests==2.27.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinx-prompt==1.5.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - toml==0.10.2
      - tomli==1.2.3
      - tox==3.28.0
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - virtualenv==20.16.2
      - zipp==3.6.0
prefix: /opt/conda/envs/cti-python-stix2

stix2/test/test_environment.py::test_object_factory_created_by_ref_str stix2/test/test_environment.py::test_object_factory_created_by_ref_obj stix2/test/test_environment.py::test_object_factory_override_default stix2/test/test_environment.py::test_object_factory_created stix2/test/test_environment.py::test_object_factory_external_resource stix2/test/test_environment.py::test_object_factory_obj_markings stix2/test/test_environment.py::test_object_factory_list_append stix2/test/test_environment.py::test_object_factory_list_replace stix2/test/test_markings.py::test_campaign_with_markings_example
stix2/test/test_markings.py::test_marking_def_example_with_tlp stix2/test/test_markings.py::test_marking_def_example_with_statement stix2/test/test_markings.py::test_marking_def_example_with_positional_statement stix2/test/test_markings.py::test_marking_def_invalid_type stix2/test/test_markings.py::test_granular_example stix2/test/test_markings.py::test_granular_example_with_bad_selector stix2/test/test_markings.py::test_campaign_with_granular_markings_example stix2/test/test_markings.py::test_parse_marking_definition[{\n stix2/test/test_markings.py::test_parse_marking_definition[data1]
stix2/environment.py stix2/other.py stix2/properties.py stix2/__init__.py
stix2/environment.py stix2/other.py stix2/properties.py stix2/__init__.py